Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 33 additions & 55 deletions internal/pkg/cli/command/index/record/search.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package record

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -20,13 +19,10 @@ import (
"github.com/pinecone-io/go-pinecone/v5/pinecone"
)

const defaultSearchTopK = 10

type searchCmdOptions struct {
indexName string
namespace string
topK int
topKExplicit bool // true when --top-k was explicitly passed (vs. the default)
inputs flags.JSONObject
filter flags.JSONObject
rerank flags.JSONObject
Expand All @@ -41,9 +37,7 @@ type searchCmdOptions struct {
}

func NewSearchCmd() *cobra.Command {
options := searchCmdOptions{
topK: defaultSearchTopK,
}
options := searchCmdOptions{}

cmd := &cobra.Command{
Use: "search",
Expand All @@ -63,7 +57,7 @@ func NewSearchCmd() *cobra.Command {
--filter metadata filter applied before ranking
--rerank re-score results using an inference model
--fields restrict which fields are returned
--top-k number of results to return (default: 10)
--top-k number of results to return (required)
--namespace target a specific namespace
--match-terms keyword terms that must appear in results; only supported for
sparse indexes with integrated embedding using the
Expand Down Expand Up @@ -96,7 +90,6 @@ func NewSearchCmd() *cobra.Command {
`),
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
options.topKExplicit = cmd.Flags().Changed("top-k")
pc := sdk.NewPineconeClient(ctx)
ic, err := sdk.NewIndexConnection(ctx, pc, options.indexName, options.namespace)
if err != nil {
Expand All @@ -112,7 +105,7 @@ func NewSearchCmd() *cobra.Command {

cmd.Flags().StringVarP(&options.indexName, "index-name", "i", "", "name of the index to search")
cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to search")
cmd.Flags().IntVarP(&options.topK, "top-k", "k", defaultSearchTopK, "number of results to return")
cmd.Flags().IntVarP(&options.topK, "top-k", "k", 0, "number of results to return")
cmd.Flags().Var(&options.inputs, "inputs", "query inputs for search (inline JSON, ./path.json, or '-' for stdin); requires integrated embedding")
cmd.Flags().Var(&options.filter, "filter", "metadata filter (inline JSON, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.rerank, "rerank", "rerank results (inline JSON, ./path.json, or '-' for stdin); required fields: model (string), rank_fields (string array)")
Expand Down Expand Up @@ -199,58 +192,43 @@ func runSearchCmd(ctx context.Context, ic RecordService, options searchCmdOption

// Merge --body into req. Flags take precedence: a flag's value in req is
// already non-nil/non-zero if the flag was set, so we only overwrite when
// the field is still unset. top-k is the exception — its default is non-zero,
// so we track whether it was explicitly passed via options.topKExplicit.
// the field is still unset. TopK == 0 means the flag was not provided;
// the body's value (including an explicit 0 or negative) is applied as-is
// and the <= 0 validation below will catch invalid values.
if options.body != "" {
rawBody, src, err := argio.ReadAll(options.body)
b, src, err := argio.DecodeJSONArg[pinecone.SearchRecordsRequest](options.body)
if err != nil {
return fmt.Errorf("failed to read search body (%s): %w", style.Emphasis(src.Label), err)
}

var bodyReq pinecone.SearchRecordsRequest
dec := json.NewDecoder(bytes.NewReader(rawBody))
dec.DisallowUnknownFields()
if err := dec.Decode(&bodyReq); err != nil {
return fmt.Errorf("failed to parse search body (%s): %w", style.Emphasis(src.Label), err)
}
// Use a pointer-field probe to detect whether top_k was explicitly
// present in the JSON body. int32's zero value is indistinguishable
// from an absent field after standard decoding, so we need this
// secondary pass to tell "top_k: 0" from a missing top_k key.
var topKProbe struct {
Query struct {
TopK *int32 `json:"top_k"`
} `json:"query"`
}
_ = json.Unmarshal(rawBody, &topKProbe)
bodyTopKSet := topKProbe.Query.TopK != nil

if !options.topKExplicit && bodyTopKSet {
req.Query.TopK = bodyReq.Query.TopK
}
if req.Query.Id == nil && bodyReq.Query.Id != nil {
req.Query.Id = bodyReq.Query.Id
}
if req.Query.Inputs == nil && bodyReq.Query.Inputs != nil {
req.Query.Inputs = bodyReq.Query.Inputs
}
if req.Query.Filter == nil && bodyReq.Query.Filter != nil {
req.Query.Filter = bodyReq.Query.Filter
}
if req.Fields == nil && bodyReq.Fields != nil {
req.Fields = bodyReq.Fields
}
if req.Query.Vector == nil && bodyReq.Query.Vector != nil {
req.Query.Vector = bodyReq.Query.Vector
}
if req.Query.MatchTerms == nil && bodyReq.Query.MatchTerms != nil {
req.Query.MatchTerms = bodyReq.Query.MatchTerms
}
if req.Rerank == nil && bodyReq.Rerank != nil {
req.Rerank = bodyReq.Rerank
if b != nil {
if req.Query.TopK == 0 {
req.Query.TopK = b.Query.TopK
}
if req.Query.Id == nil && b.Query.Id != nil {
req.Query.Id = b.Query.Id
}
if req.Query.Inputs == nil && b.Query.Inputs != nil {
req.Query.Inputs = b.Query.Inputs
}
if req.Query.Filter == nil && b.Query.Filter != nil {
req.Query.Filter = b.Query.Filter
}
if req.Fields == nil && b.Fields != nil {
req.Fields = b.Fields
}
if req.Query.Vector == nil && b.Query.Vector != nil {
req.Query.Vector = b.Query.Vector
}
if req.Query.MatchTerms == nil && b.Query.MatchTerms != nil {
req.Query.MatchTerms = b.Query.MatchTerms
}
if req.Rerank == nil && b.Rerank != nil {
req.Rerank = b.Rerank
}
}
}


if req.Query.TopK <= 0 {
return fmt.Errorf("top-k must be greater than 0")
}
Expand Down
47 changes: 19 additions & 28 deletions internal/pkg/cli/command/index/record/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,7 @@ func Test_runSearchCmd_BodyProvidesQuery(t *testing.T) {
err := runSearchCmd(context.Background(), svc, searchCmdOptions{
indexName: "my-index",
namespace: "__default__",
topK: defaultSearchTopK,
// No query flags set; body supplies the id.
// No query flags set; body supplies both id and top_k.
body: `{"query":{"top_k":3,"id":"body-rec"}}`,
})

Expand All @@ -247,7 +246,7 @@ func Test_runSearchCmd_FlagIdWinsOverBody(t *testing.T) {
err := runSearchCmd(context.Background(), svc, searchCmdOptions{
indexName: "my-index",
namespace: "__default__",
topK: defaultSearchTopK,
topK: 10,
id: "flag-rec",
body: `{"query":{"id":"body-rec"}}`,
})
Expand All @@ -257,29 +256,27 @@ func Test_runSearchCmd_FlagIdWinsOverBody(t *testing.T) {
assert.Equal(t, "flag-rec", *svc.lastSearchReq.Query.Id)
}

func Test_runSearchCmd_ExplicitTopKWinsOverBody(t *testing.T) {
func Test_runSearchCmd_FlagTopKWinsOverBody(t *testing.T) {
svc := &mockRecordService{searchResp: emptySearchResp}
err := runSearchCmd(context.Background(), svc, searchCmdOptions{
indexName: "my-index",
namespace: "__default__",
topK: 20,
topKExplicit: true,
id: "rec-1",
body: `{"query":{"top_k":99,"id":"rec-1"}}`,
indexName: "my-index",
namespace: "__default__",
topK: 20,
id: "rec-1",
body: `{"query":{"top_k":99,"id":"rec-1"}}`,
})

require.NoError(t, err)
assert.Equal(t, int32(20), svc.lastSearchReq.Query.TopK)
}

func Test_runSearchCmd_BodyTopKAppliedWhenNotExplicit(t *testing.T) {
func Test_runSearchCmd_BodyTopKAppliedWhenFlagAbsent(t *testing.T) {
svc := &mockRecordService{searchResp: emptySearchResp}
err := runSearchCmd(context.Background(), svc, searchCmdOptions{
indexName: "my-index",
namespace: "__default__",
topK: defaultSearchTopK, // default, not explicitly set
topKExplicit: false,
body: `{"query":{"top_k":7,"id":"rec-1"}}`,
indexName: "my-index",
namespace: "__default__",
// topK not set (0); body supplies it.
body: `{"query":{"top_k":7,"id":"rec-1"}}`,
})

require.NoError(t, err)
Expand All @@ -289,13 +286,9 @@ func Test_runSearchCmd_BodyTopKAppliedWhenNotExplicit(t *testing.T) {
func Test_runSearchCmd_BodyZeroTopKIsValidationError(t *testing.T) {
svc := &mockRecordService{searchResp: emptySearchResp}
err := runSearchCmd(context.Background(), svc, searchCmdOptions{
indexName: "my-index",
namespace: "__default__",
topK: defaultSearchTopK, // default, not explicitly set
topKExplicit: false,
// body explicitly sets top_k to 0; should be rejected, not silently
// replaced with the default of 10.
body: `{"query":{"top_k":0,"id":"rec-1"}}`,
indexName: "my-index",
namespace: "__default__",
body: `{"query":{"top_k":0,"id":"rec-1"}}`,
})

require.Error(t, err)
Expand All @@ -306,11 +299,9 @@ func Test_runSearchCmd_BodyZeroTopKIsValidationError(t *testing.T) {
func Test_runSearchCmd_BodyNegativeTopKIsValidationError(t *testing.T) {
svc := &mockRecordService{searchResp: emptySearchResp}
err := runSearchCmd(context.Background(), svc, searchCmdOptions{
indexName: "my-index",
namespace: "__default__",
topK: defaultSearchTopK,
topKExplicit: false,
body: `{"query":{"top_k":-5,"id":"rec-1"}}`,
indexName: "my-index",
namespace: "__default__",
body: `{"query":{"top_k":-5,"id":"rec-1"}}`,
})

require.Error(t, err)
Expand Down
5 changes: 3 additions & 2 deletions internal/pkg/cli/command/index/record/upsert.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ func NewUpsertCmd() *cobra.Command {

cmd.Flags().StringVarP(&options.indexName, "index-name", "i", "", "name of index to upsert into")
cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to upsert into")
cmd.Flags().StringVar(&options.file, "file", "", "request body JSON or JSONL (inline, ./path.json[l], or '-' for stdin; only one argument may use stdin)")
cmd.Flags().StringVar(&options.file, "body", "", "alias for --file")
cmd.Flags().StringVar(&options.file, "body", "", "request body JSON or JSONL (inline, ./path.json[l], or '-' for stdin; only one argument may use stdin)")
cmd.Flags().StringVar(&options.file, "file", "", "alias for --body")
_ = cmd.Flags().MarkHidden("file")
cmd.Flags().IntVarP(&options.batchSize, "batch-size", "b", 96, "records per batch (max 96)")
cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON")

Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/command/index/vector/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func NewQueryCmd() *cobra.Command {
cmd.Flags().BoolVar(&options.includeMetadata, "include-metadata", false, "include metadata in the query results")
cmd.Flags().StringVar(&options.id, "id", "", "ID of the vector to query against")
cmd.Flags().VarP(&options.vector, "vector", "v", "vector values to query against (inline JSON array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.sparseIndices, "sparse-indices", "sparse indices to query against (inline JSON array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.sparseIndices, "sparse-indices", "sparse indices to query against (inline JSON uint32 array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.sparseValues, "sparse-values", "sparse values to query against (inline JSON array, ./path.json, or '-' for stdin)")
cmd.Flags().StringVar(&options.body, "body", "", "request body JSON (inline, ./path.json, or '-' for stdin; only one argument may use stdin)")
cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON")
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/command/index/vector/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func NewUpdateCmd() *cobra.Command {
cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to update the vector in")
cmd.Flags().StringVar(&options.id, "id", "", "ID of the vector to update")
cmd.Flags().Var(&options.values, "values", "values to update the vector with (inline JSON array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.sparseIndices, "sparse-indices", "sparse indices to update the vector with (inline JSON array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.sparseIndices, "sparse-indices", "sparse indices to update the vector with (inline JSON uint32 array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.sparseValues, "sparse-values", "sparse values to update the vector with (inline JSON array, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.metadata, "metadata", "metadata to update the vector with (inline JSON, ./path.json, or '-' for stdin)")
cmd.Flags().Var(&options.filter, "filter", "filter to update the vectors with (inline JSON, ./path.json, or '-' for stdin)")
Expand Down
5 changes: 3 additions & 2 deletions internal/pkg/cli/command/index/vector/upsert.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ func NewUpsertCmd() *cobra.Command {

cmd.Flags().StringVarP(&options.indexName, "index-name", "i", "", "name of index to upsert into")
cmd.Flags().StringVar(&options.namespace, "namespace", "", "namespace to upsert into")
cmd.Flags().StringVar(&options.file, "file", "", "request body JSON or JSONL (inline, ./path.json[l], or '-' for stdin; only one argument may use stdin)")
cmd.Flags().StringVar(&options.file, "body", "", "alias for --file")
cmd.Flags().StringVar(&options.file, "body", "", "request body JSON or JSONL (inline, ./path.json[l], or '-' for stdin; only one argument may use stdin)")
cmd.Flags().StringVar(&options.file, "file", "", "alias for --body")
_ = cmd.Flags().MarkHidden("file")
cmd.Flags().IntVarP(&options.batchSize, "batch-size", "b", 500, "size of batches to upsert (default: 500)")
cmd.Flags().BoolVarP(&options.json, "json", "j", false, "output as JSON")
_ = cmd.MarkFlagRequired("index-name")
Expand Down
Loading