Skip to content
Open
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
31 changes: 11 additions & 20 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ universal_binaries:
- replace: true

archives:
- format: tar.gz
- formats: ['tar.gz']
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
Expand All @@ -45,7 +45,7 @@ archives:
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
formats: ['zip']

report_sizes: true

Expand All @@ -69,7 +69,7 @@ release:
owner: pinecone-io
name: cli

brews:
homebrew_casks:
- name: pinecone
homepage: "https://www.pinecone.io"
description: "Pinecone CLI"
Expand All @@ -81,21 +81,12 @@ brews:
commit_author:
name: goreleaserbot
email: bot@goreleaser.com
commit_msg_template: "Brew formula update for pinecone version {{ .Tag }}"
commit_msg_template: "Brew cask update for pinecone version {{ .Tag }}"
skip_upload: auto
directory: Formula
license: "Apache-2.0"
test: |
system "#{bin}/pc --help"
install: |
bin.install "pc"
bin.install_symlink "pc" => "pinecone"

# Install man pages
man1.install Dir["man/man1/*.1"]

# Add aliases: pc*.1 -> pinecone*.1, etc
Dir[man1/"pc*.1"].each do |src|
dest = File.basename(src).sub(/\Apc\b/, "pinecone") # Replace leading "pc" with "pinecone"
man1.install_symlink src => dest
end
directory: Casks
binaries:
- pc
manpages:
- "man/man1/*.1"
custom_block: |
binary "pc", target: "pinecone"
17 changes: 17 additions & 0 deletions cmd/gen-manpages/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ func main() {
os.Exit(1)
}

// Create pinecone*.1 symlinks for each pc*.1 man page so that
// `man pinecone` and `man pinecone-index` etc. work in addition to `man pc`.
pcPages, err := filepath.Glob(filepath.Join(*output, "pc*.1"))
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to list man page files: %s\n", err)
} else {
for _, src := range pcPages {
base := filepath.Base(src)
dest := "pinecone" + base[len("pc"):]
destPath := filepath.Join(*output, dest)
os.Remove(destPath)
if err := os.Symlink(base, destPath); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to create symlink %s -> %s: %s\n", dest, base, err)
}
}
}

// List generated files for verbose
if *verbose {
files, err := filepath.Glob(filepath.Join(*output, "*.1"))
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/charmbracelet/lipgloss v0.10.0
github.com/fatih/color v1.16.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/pinecone-io/go-pinecone/v5 v5.2.0
github.com/pinecone-io/go-pinecone/v5 v5.4.1
github.com/rs/zerolog v1.32.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQ
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pinecone-io/go-pinecone/v5 v5.2.0 h1:QlkWUdjctCq+846m7bVhGNor+6PS2fubhjf55Uz4glM=
github.com/pinecone-io/go-pinecone/v5 v5.2.0/go.mod h1:6Fg85fcyvMUQFf9KW7zniN81kelSYvsjF+KPLdc1MGA=
github.com/pinecone-io/go-pinecone/v5 v5.4.1 h1:JJJ4VIu5NpFc3BIRcjc93n/XxYtACwRjRI/e6eHoOIU=
github.com/pinecone-io/go-pinecone/v5 v5.4.1/go.mod h1:6Fg85fcyvMUQFf9KW7zniN81kelSYvsjF+KPLdc1MGA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/cli/command/index/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package index

import (
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/namespace"
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/record"
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/vector"
"github.com/pinecone-io/cli/internal/pkg/utils/help"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -42,6 +43,7 @@ func NewIndexCmd() *cobra.Command {
cmd.AddCommand(NewDescribeIndexStatsCmd())

cmd.AddGroup(help.GROUP_INDEX_DATA)
cmd.AddCommand(record.NewRecordCmd())
cmd.AddCommand(vector.NewVectorCmd())

cmd.AddGroup(help.GROUP_INDEX_NAMESPACE)
Expand Down
31 changes: 31 additions & 0 deletions internal/pkg/cli/command/index/record/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package record

import (
"github.com/pinecone-io/cli/internal/pkg/utils/help"
"github.com/spf13/cobra"
)

func NewRecordCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "record",
Short: "Work with records in an index",
Long: help.Long(`
Work with records in an integrated Pinecone index.

Use these commands to upsert raw records and run semantic search against
them. All commands require --index-name and may optionally target a
--namespace.
`),
Example: help.Examples(`
pc index record upsert --index-name my-index --namespace my-namespace --body ./records.jsonl
pc index record search --index-name my-index --namespace my-namespace --inputs '{"text":"search query"}'
pc index record search --index-name my-index --namespace my-namespace --body ./search.json
`),
GroupID: help.GROUP_INDEX_DATA.ID,
}

cmd.AddCommand(NewUpsertCmd())
cmd.AddCommand(NewSearchCmd())

return cmd
}
189 changes: 189 additions & 0 deletions internal/pkg/cli/command/index/record/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package record

import (
"encoding/json"

"github.com/pinecone-io/cli/internal/pkg/utils/argio"
"github.com/pinecone-io/cli/internal/pkg/utils/exit"
"github.com/pinecone-io/cli/internal/pkg/utils/flags"
"github.com/pinecone-io/cli/internal/pkg/utils/help"
"github.com/pinecone-io/cli/internal/pkg/utils/msg"
"github.com/pinecone-io/cli/internal/pkg/utils/pcio"
"github.com/pinecone-io/cli/internal/pkg/utils/presenters"
"github.com/pinecone-io/cli/internal/pkg/utils/sdk"
"github.com/pinecone-io/cli/internal/pkg/utils/style"
"github.com/pinecone-io/cli/internal/pkg/utils/text"
"github.com/spf13/cobra"

"github.com/pinecone-io/go-pinecone/v5/pinecone"
)

const defaultSearchTopK = 10

type searchCmdOptions struct {
indexName string
namespace string
topK int
inputs flags.JSONObject
filter flags.JSONObject
rerank flags.JSONObject
id string
fields flags.StringList
body string
json bool
}

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

cmd := &cobra.Command{
Use: "search",
Short: "Search records in an integrated index",
Long: help.Long(`
Run semantic search against records in an integrated index.

Provide query text via --inputs (inline JSON, ./path.json, or '-' for stdin).
Narrow results with --filter (metadata filter as a JSON object).
Rerank results with --rerank (JSON object with required fields: model, rank_fields).
Use --body to supply a full request body for advanced parameters like
vector overrides or match_terms.

When a flag and --body both specify the same field, the flag takes precedence.
`),
Example: help.Examples(`
pc index record search --index-name my-index --namespace my-namespace --inputs '{"text":"find similar"}'
pc index record search --index-name my-index --inputs '{"text":"disease prevention"}' --filter '{"category":"health"}'
pc index record search --index-name my-index --inputs '{"text":"find similar"}' --rerank '{"model":"bge-reranker-v2-m3","rank_fields":["chunk_text"]}'
echo '{"text":"disease prevention"}' | pc index record search --index-name my-index --inputs -
pc index record search --index-name my-index --namespace my-namespace --body ./search.json
`),
Run: func(cmd *cobra.Command, args []string) {
runSearchCmd(cmd, options)
},
}

cmd.Flags().StringVarP(&options.indexName, "index-name", "n", "", "name of the index to search")
cmd.Flags().StringVar(&options.namespace, "namespace", "__default__", "namespace to search")
cmd.Flags().IntVarP(&options.topK, "top-k", "k", defaultSearchTopK, "number of results to return")
cmd.Flags().Var(&options.inputs, "inputs", "query inputs for search (inline JSON, ./path.json, or '-' for stdin)")
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)")
cmd.Flags().StringVar(&options.id, "id", "", "use an existing record's vector by ID for the query")
cmd.Flags().Var(&options.fields, "fields", "fields to return in results (JSON string 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")

_ = cmd.MarkFlagRequired("index-name")

return cmd
}

func runSearchCmd(cmd *cobra.Command, options searchCmdOptions) {
ctx := cmd.Context()
pc := sdk.NewPineconeClient(ctx)

// Build req from flags.
req := pinecone.SearchRecordsRequest{
Query: pinecone.SearchRecordsQuery{
TopK: int32(options.topK),
},
}

if options.id != "" {
req.Query.Id = &options.id
}
if options.inputs != nil {
inputs := map[string]interface{}(options.inputs)
req.Query.Inputs = &inputs
}
if options.filter != nil {
filter := map[string]interface{}(options.filter)
req.Query.Filter = &filter
}
if len(options.fields) > 0 {
fieldsCopy := make([]string, len(options.fields))
copy(fieldsCopy, options.fields)
req.Fields = &fieldsCopy
}
if options.rerank != nil {
b, err := json.Marshal(options.rerank)
if err != nil {
msg.FailMsg("Failed to encode --rerank value: %s", err)
exit.Error(err, "Failed to encode --rerank value")
}
var rerank pinecone.SearchRecordsRerank
if err := json.Unmarshal(b, &rerank); err != nil {
msg.FailMsg("Failed to parse --rerank value: %s", err)
exit.Error(err, "Failed to parse --rerank value")
}
req.Rerank = &rerank
}

// Merge --body into req. For fields that have a dedicated flag, the flag
// takes precedence; body only fills in values that weren't explicitly set.
// Fields with no dedicated flag (Vector, MatchTerms, Rerank) always come
// from body.
if options.body != "" {
b, src, err := argio.DecodeJSONArg[pinecone.SearchRecordsRequest](options.body)
if err != nil {
msg.FailMsg("Failed to parse search body (%s): %s", style.Emphasis(src.Label), err)
exit.Errorf(err, "Failed to parse search body (%s)", src.Label)
}
if b != nil {
if !cmd.Flags().Changed("top-k") && b.Query.TopK > 0 {
req.Query.TopK = b.Query.TopK
}
if !cmd.Flags().Changed("id") && b.Query.Id != nil {
req.Query.Id = b.Query.Id
}
if !cmd.Flags().Changed("inputs") && b.Query.Inputs != nil {
req.Query.Inputs = b.Query.Inputs
}
if !cmd.Flags().Changed("filter") && b.Query.Filter != nil {
req.Query.Filter = b.Query.Filter
}
if !cmd.Flags().Changed("fields") && b.Fields != nil {
req.Fields = b.Fields
}
if b.Query.Vector != nil {
req.Query.Vector = b.Query.Vector
}
if b.Query.MatchTerms != nil {
req.Query.MatchTerms = b.Query.MatchTerms
}
if !cmd.Flags().Changed("rerank") && b.Rerank != nil {
req.Rerank = b.Rerank
}
}
}

if req.Query.TopK <= 0 {
msg.FailMsg("Top-k must be greater than 0")
exit.ErrorMsg("Invalid top-k value")
}

if req.Query.Id == nil && req.Query.Inputs == nil && req.Query.Vector == nil && req.Query.MatchTerms == nil {
msg.FailMsg("Provide --inputs, --id, or a body with vector/match_terms")
exit.ErrorMsg("Missing query inputs for search")
}

ic, err := sdk.NewIndexConnection(ctx, pc, options.indexName, options.namespace)
if err != nil {
msg.FailMsg("Failed to create index connection: %s", err)
exit.Error(err, "Failed to create index connection")
}

resp, err := ic.SearchRecords(ctx, &req)
if err != nil {
exit.Error(err, "Failed to search records")
}

if options.json {
json := text.IndentJSON(resp)
pcio.Println(json)
} else {
presenters.PrintSearchRecordsTable(resp)
}
}
Loading