Skip to content

Commit

Permalink
feat(Search): added implementations for core search and commandline s…
Browse files Browse the repository at this point in the history
…earch
  • Loading branch information
Thomas Osterbind committed Jun 12, 2018
1 parent 51e792b commit 698a53b
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 125 deletions.
26 changes: 26 additions & 0 deletions cmd/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/qri-io/qri/config"
"github.com/qri-io/qri/core"
"github.com/qri-io/qri/repo"
"github.com/qri-io/registry"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -129,6 +130,31 @@ func printDatasetRefInfo(w io.Writer, i int, ref repo.DatasetRef) {
fmt.Fprintln(w)
}

func printSearchResult(i int, result core.SearchResult) {
white := color.New(color.FgWhite).SprintFunc()
green := color.New(color.FgGreen).SprintFunc()
// NOTE: in the future we need to switch based on result.Type
// For now we are taking a shortcut and assuming a dataset struct
dsname := result.ID
resultStruct := &registry.Dataset{}
resultString := dsname
bytes, err := json.Marshal(result.Value)
if err == nil {
err = json.Unmarshal(bytes, resultStruct)
if err == nil {
var title, desc string
if resultStruct != nil {
if resultStruct.Meta != nil {
title = resultStruct.Meta.Title
desc = strings.Replace(resultStruct.Meta.Description, "\n", "\n ", -1)
}
}
resultString = fmt.Sprintf("%s\n %s\n %s\n", white(dsname), green(title), desc)
}
}
fmt.Printf("%s. %s", white(i+1), resultString)
}

func printPeerInfo(w io.Writer, i int, p *config.ProfilePod) {
white := color.New(color.FgWhite).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
Expand Down
2 changes: 1 addition & 1 deletion cmd/qri.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func NewQriCommand(pf PathFactory, in io.Reader, out, err io.Writer) *cobra.Comm
NewRenameCommand(opt, ioStreams),
NewRenderCommand(opt, ioStreams),
NewSaveCommand(opt, ioStreams),
// NewSearchCommand(opt, ioStreams),
NewSearchCommand(opt, ioStreams),
NewSetupCommand(opt, ioStreams),
NewValidateCommand(opt, ioStreams),
NewVersionCommand(opt, ioStreams),
Expand Down
42 changes: 18 additions & 24 deletions cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/qri-io/dataset"
"github.com/qri-io/qri/core"
"github.com/qri-io/qri/repo"
"github.com/spf13/cobra"
)

Expand All @@ -27,7 +26,6 @@ func NewSearchCommand(f Factory, ioStreams IOStreams) *cobra.Command {
},
}

cmd.Flags().BoolVarP(&o.Reindex, "reindex", "r", false, "re-generate search index from scratch. might take a while.")
cmd.Flags().StringVarP(&o.Format, "format", "f", "", "set output format [json]")

return cmd
Expand All @@ -36,12 +34,13 @@ func NewSearchCommand(f Factory, ioStreams IOStreams) *cobra.Command {
// SearchOptions encapsulates state for the search command
type SearchOptions struct {
IOStreams

Query string
Format string
Reindex bool

Query string
SearchRequests *core.SearchRequests
Format string
// TODO: add support for specifying limit and offset
// Limit int
// Offset int
// Reindex bool
}

// Complete adds any missing configuration that can only be added just before calling Run
Expand All @@ -54,33 +53,28 @@ func (o *SearchOptions) Complete(f Factory, args []string) (err error) {
// Run executes the search command
func (o *SearchOptions) Run() (err error) {

if o.Reindex {
printInfo(o.Out, "building index...")
done := false
if err = o.SearchRequests.Reindex(&core.ReindexSearchParams{}, &done); err != nil {
return err
}
printSuccess(o.Out, "reindex complete")
}
// TODO: add reindex option back in

p := &repo.SearchParams{
Q: o.Query,
Limit: 30,
Offset: 0,
p := &core.SearchParams{
QueryString: o.Query,
Limit: 100,
Offset: 0,
}
res := []repo.DatasetRef{}
results := &[]core.SearchResult{}

if err = o.SearchRequests.Search(p, &res); err != nil {
if err = o.SearchRequests.Search(p, results); err != nil {
return err
}

switch o.Format {
case "":
for i, ref := range res {
printDatasetRefInfo(o.Out, i, ref)
fmt.Printf("showing %d results for '%s'\n", len(*results), o.Query)
for i, result := range *results {
printSearchResult(i, result)
}

case dataset.JSONDataFormat.String():
data, err := json.MarshalIndent(res, "", " ")
data, err := json.MarshalIndent(results, "", " ")
if err != nil {
return err
}
Expand Down
88 changes: 37 additions & 51 deletions core/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,68 @@ import (
"fmt"
"net/rpc"

"github.com/qri-io/cafs"
"github.com/qri-io/qri/repo"
"github.com/qri-io/qri/repo/fs"
"github.com/qri-io/qri/repo/actions"
"github.com/qri-io/registry/regclient"
)

// SearchRequests encapsulates business logic for the qri search
// command
type SearchRequests struct {
store cafs.Filestore
repo repo.Repo
// node *p2p.QriNode
cli *rpc.Client
cli *rpc.Client
repo repo.Repo
}

// CoreRequestsName implements the requests
func (d SearchRequests) CoreRequestsName() string { return "search" }

// NewSearchRequests creates a SearchRequests pointer from either a repo
// or an rpc.Client
func NewSearchRequests(r repo.Repo, cli *rpc.Client) *SearchRequests {
if r != nil && cli != nil {
panic(fmt.Errorf("both repo and client supplied to NewSearchRequests"))
}

return &SearchRequests{
repo: r,
// node: node,
cli: cli,
cli: cli,
repo: &actions.Registry{r},
}
}

// Search queries for items on qri related to given parameters
func (d *SearchRequests) Search(p *repo.SearchParams, res *[]repo.DatasetRef) error {
if d.cli != nil {
return d.cli.Call("SearchRequests.Search", p, res)
}
// if d.node != nil {
// r, err := d.node.Search(p.Query, p.Limit, p.Offset)
// if err != nil {
// return err
// }

if searchable, ok := d.repo.(repo.Searchable); ok {
results, err := searchable.Search(*p)
if err != nil {
log.Debug(err.Error())
return fmt.Errorf("error searching: %s", err.Error())
}
*res = results
return nil
}
// CoreRequestsName implements the requests
func (sr SearchRequests) CoreRequestsName() string { return "search" }

return fmt.Errorf("this repo doesn't support search")
// SearchParams defines paremeters for the search Method
type SearchParams struct {
QueryString string
Limit int
Offset int
}

// ReindexSearchParams defines parmeters for
// the Reindex method
type ReindexSearchParams struct {
// no args for reindex
// SearchResult struct
type SearchResult struct {
Type, ID string
Value interface{}
}

// Reindex instructs a qri node to re-calculate it's search index
func (d *SearchRequests) Reindex(p *ReindexSearchParams, done *bool) error {
if d.cli != nil {
return d.cli.Call("SearchRequests.Reindex", p, done)
// Search queries for items on qri related to given parameters
func (sr *SearchRequests) Search(p *SearchParams, results *[]SearchResult) error {
if sr.cli != nil {
return sr.cli.Call("SearchRequests.Search", p, results)
}
reg := sr.repo.Registry()
if p == nil {
return fmt.Errorf("error: search params cannot be nil")
}
params := &regclient.SearchParams{p.QueryString, nil, p.Limit, p.Offset}

if fsr, ok := d.repo.(*fsrepo.Repo); ok {
err := fsr.UpdateSearchIndex(d.repo.Store())
if err != nil {
log.Debug(err.Error())
return fmt.Errorf("error reindexing: %s", err.Error())
}
*done = true
return nil
regResults, err := reg.Search(params)
if err != nil {
return err
}

return fmt.Errorf("search reindexing is currently only supported on file-system repos")
searchResults := make([]SearchResult, len(regResults))
for i, result := range regResults {
searchResults[i].Type = result.Type
searchResults[i].ID = result.ID
searchResults[i].Value = result.Value
}
*results = searchResults
return nil
}
72 changes: 23 additions & 49 deletions core/search_test.go
Original file line number Diff line number Diff line change
@@ -1,71 +1,45 @@
package core

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/qri-io/qri/repo"
testrepo "github.com/qri-io/qri/repo/test"
"github.com/qri-io/registry/regclient"
)

var mockResponse = []byte(`{"data":[{"Type":"","ID":"ramfox/test","Value":null},{"Type":"","ID":"b5/test","Value":{"commit":{"path":"/","signature":"JI/VSNqMuFGYVEwm3n8ZMjZmey+W2mhkD5if2337wDp+kaYfek9DntOyZiILXocW5JuOp48EqcsWf/BwhejQiYZ2utaIzR8VcMPo7u7c5nz2G6JTsoW+u9UUaKRVtl30jh6Kg1O2bnhYh9v4qW9VQgxOYfdhBl6zT4cYcjm1UkrblEe/wh494k9NziM5Bi2ATGRE2m71Lsf/TEDoNI549SebLQ1dsWXr1kM7lCeFqlDVjgbKQmGXowqcK/P9v+RBIRCnArnwFe/BQq4i1wmmnMEqpuUnfWR3xfJTE1DUMVaAid7U0jTWGVxROUdKk6mmTzlb1PiNdfruP+SFhjyQwQ==","timestamp":"2018-05-25T13:44:54.692493401Z","title":"created dataset"},"meta":{"citations":[{"url":"https://api.github.com/repos/qri-io/qri/releases"}],"qri":"md:0"},"path":"/ipfs/QmPi5wrPsY4xPwy2oRr7NRZyfFxTeupfmnrVDubzoABLNP","qri":"","structure":{"checksum":"QmQXfdYYubCvr9ePJtgABpp7N1fNsAnnywUJPYAJTvDhrn","errCount":0,"entries":7,"format":"json","length":19116,"qri":"","schema":{"type":"array"}},"transform":{"config":{"org":"qri-io","repo":"qri"},"path":"/","syntax":"skylark"},"Handle":"b5","Name":"test","PublicKey":"CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/W17VPFX+pjyM1MHI1CsvIe+JYQX45MJNITTd7hZZDX2rRWVavGXhsccmVDGU6ubeN3t6ewcBlgCxvyewwKhmZKCAs3/0xNGKXK/YMyZpRVjTWw9yPU9gOzjd9GuNJtL7d1Hl7dPt9oECa7WBCh0W9u2IoHTda4g8B2mK92awLOZTjXeA7vbhKKX+QVHKDxEI0U2/ooLYJUVxEoHRc+DUYNPahX5qRgJ1ZDP4ep1RRRoZR+HjGhwgJP+IwnAnO5cRCWUbZvE1UBJUZDvYMqW3QvDp+TtXwqUWVvt69tp8EnlBgfyXU91A58IEQtLgZ7klOzdSEJDP+S8HIwhG/vbTAgMBAAE="}},{"Type":"","ID":"EDGI/fib_6","Value":{"commit":{"path":"/","signature":"dG6NoEFlQ9ILFjVDrecSDlUbDPRSiwK9kFQ3vjTh/4tpfgrT5EOw6eGDx75lklx0DWx51s3AC2Qqytll2JwwCB6SMVVl0I9qnZJ4XQVG+MX4hGeIJ4crGCSts85unDvmiQCfc4EVqPYZKLzaVqjXa43zv5mJPfRA2ktTew3VGkOcg8RmyU6e2XWrZpkILcZg1jt2apKs5qHslx4klKBVPtQIr53/U61OW4tzME9kz08FYrk2F7I5FHWy45W7VU8DpzCbhw6kxJXu2KYD1QstsZGCKH93sZY3agP4XGY15HeEOTib465LK6+nsoBtrsroQSOTBHzVgyUZACNom5SUvQ==","timestamp":"2018-05-23T19:50:03.307982846Z","title":"created dataset"},"meta":{"description":"test of skylark as a transformation language","qri":"md:0","title":"Fibonacci(6)"},"path":"/ipfs/QmS6jJSEJYxZvCeo8cZqzVa7Ybu9yNQeFYfNZAHxM4eyDK","qri":"","structure":{"checksum":"QmdhDDZTAWoifKCWwFrZqzKUyXM6rN7ThdP1u67rkeJvTj","errCount":0,"entries":6,"format":"cbor","length":7,"qri":"","schema":{"type":"array"}},"transform":{"syntax":"skylark"},"Handle":"EDGI","Name":"fib_6","PublicKey":"CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmTFRx/6dKmoxje8AG+jFv94IcGUGnjrupa7XEr12J/c4ZLn3aPrD8F0tjRbstt1y/J+bO7Qb69DGiu2iSIqyE21nl2oex5+14jtxbupRq9jRTbpUHRj+y9I7uUDwl0E2FS1IQpBBfEGzDPIBVavxbhguC3O3XA7Aq7vea2lpJ1tWpr0GDRYSNmJAybkHS6k7dz1eVXFK+JE8FGFJi/AThQZKWRijvWFdlZvb8RyNFRHzpbr9fh38bRMTqhZpw/YGO5Ly8PNSiOOE4Y5cNUHLEYwG2/lpT4l53iKScsaOazlRkJ6NmkM1il7riCa55fcIAQZDtaAx+CT5ZKfmek4P5AgMBAAE="}}],"meta":{"code":200}}`)

func TestSearch(t *testing.T) {
cases := []struct {
p *repo.SearchParams
res []repo.DatasetRef
err string
}{
{&repo.SearchParams{}, nil, "this repo doesn't support search"},
}

mr, err := testrepo.NewTestRepo(nil)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(mockResponse)
}))
rc := regclient.NewClient(&regclient.Config{Location: server.URL})
mr, err := testrepo.NewTestRepo(rc)
if err != nil {
t.Errorf("error allocating test repo: %s", err.Error())
return
}

// Case 0 - request with expected result
i := 0
p := &SearchParams{"cities", 0, 100}
numResults := 3
errString := ""

// make request 0
req := NewSearchRequests(mr, nil)
for i, c := range cases {
got := []repo.DatasetRef{}
err := req.Search(c.p, &got)

if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
t.Errorf("case %d error mismatch: expected: %s, got: %s", i, c.err, err)
continue
}
got := &[]SearchResult{}
err = req.Search(p, got)

if len(c.res) != len(got) {
t.Errorf("case %d log count mismatch. expected: %d, got: %d", i, len(c.res), len(got))
continue
}
if !(err == nil && errString == "" || err != nil && err.Error() == errString) {
t.Errorf("case %d error mismatch: expected: %s, got: %s", i, errString, err)
}
}

func TestReindex(t *testing.T) {
cases := []struct {
p *ReindexSearchParams
expect bool
err string
}{
{&ReindexSearchParams{}, false, "search reindexing is currently only supported on file-system repos"},
}

mr, err := testrepo.NewTestRepo(nil)
if err != nil {
t.Errorf("error allocating test repo: %s", err.Error())
return
}

req := NewSearchRequests(mr, nil)
for i, c := range cases {
got := false
err := req.Reindex(c.p, &got)

if !(err == nil && c.err == "" || err != nil && err.Error() == c.err) {
t.Errorf("case %d error mismatch: expected: %s, got: %s", i, c.err, err)
continue
}
if got != c.expect {
t.Errorf("case %d expected: %t got: %t", i, c.expect, got)
continue
}
if len(*got) != numResults {
t.Errorf("case %d result count mismatch: expected: %d results, got: %d", i, numResults, len(*got))
}
}

0 comments on commit 698a53b

Please sign in to comment.