diff --git a/internal/pkg/cli/command/index/record/search.go b/internal/pkg/cli/command/index/record/search.go index 43e0d9b..b1116f6 100644 --- a/internal/pkg/cli/command/index/record/search.go +++ b/internal/pkg/cli/command/index/record/search.go @@ -1,7 +1,6 @@ package record import ( - "bytes" "context" "encoding/json" "fmt" @@ -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 @@ -41,9 +37,7 @@ type searchCmdOptions struct { } func NewSearchCmd() *cobra.Command { - options := searchCmdOptions{ - topK: defaultSearchTopK, - } + options := searchCmdOptions{} cmd := &cobra.Command{ Use: "search", @@ -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 @@ -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 { @@ -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)") @@ -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") } diff --git a/internal/pkg/cli/command/index/record/search_test.go b/internal/pkg/cli/command/index/record/search_test.go index 356b24e..8211054 100644 --- a/internal/pkg/cli/command/index/record/search_test.go +++ b/internal/pkg/cli/command/index/record/search_test.go @@ -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"}}`, }) @@ -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"}}`, }) @@ -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) @@ -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) @@ -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) diff --git a/internal/pkg/cli/command/index/record/upsert.go b/internal/pkg/cli/command/index/record/upsert.go index e910634..fcce400 100644 --- a/internal/pkg/cli/command/index/record/upsert.go +++ b/internal/pkg/cli/command/index/record/upsert.go @@ -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") diff --git a/internal/pkg/cli/command/index/vector/query.go b/internal/pkg/cli/command/index/vector/query.go index 738ea59..a742768 100644 --- a/internal/pkg/cli/command/index/vector/query.go +++ b/internal/pkg/cli/command/index/vector/query.go @@ -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") diff --git a/internal/pkg/cli/command/index/vector/update.go b/internal/pkg/cli/command/index/vector/update.go index cf84433..524fc21 100644 --- a/internal/pkg/cli/command/index/vector/update.go +++ b/internal/pkg/cli/command/index/vector/update.go @@ -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)") diff --git a/internal/pkg/cli/command/index/vector/upsert.go b/internal/pkg/cli/command/index/vector/upsert.go index eb0fb04..5365907 100644 --- a/internal/pkg/cli/command/index/vector/upsert.go +++ b/internal/pkg/cli/command/index/vector/upsert.go @@ -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")