diff --git a/cmd/doc.go b/cmd/doc.go index b6d2508..ee0bc0a 100644 --- a/cmd/doc.go +++ b/cmd/doc.go @@ -34,7 +34,7 @@ func Doc(conf *cfg.Config) *cli.Command { Commands: []*cli.Command{ DocAdd(conf), DocShow(conf), - //Delete(conf), + DocDelete(conf), }, } } @@ -112,3 +112,66 @@ func DocShow(conf *cfg.Config) *cli.Command { }, } } + +func DocDelete(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "delete", + Aliases: []string{"rm"}, + Usage: "delete JSON document[s] from index[es]", + UsageText: "delete [options] -i [<[field]pattern> ...]\n" + SearchUsage, + + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Usage: "delete all docs (dangerous!)", + Destination: &conf.All, + Aliases: []string{"a"}, + }, + &cli.StringFlag{ + Name: "index", + Usage: "index to work on", + Sources: cli.EnvVars("ES_INDEX"), + Destination: &conf.Index, + Aliases: []string{"i"}, + }, + &cli.StringSliceFlag{ + Name: "filter", + Usage: "additional boolean filters. format: key=value", + Destination: &conf.Filter, + Aliases: []string{"F"}, + }, + &cli.StringFlag{ + Name: "timerange", + Usage: "field: to (e.g. @timestamp:2026-05-05 to 2026-05-15)", + Destination: &conf.Range, + Aliases: []string{"r"}, + }, + &cli.StringFlag{ + Name: "timestamp-format", + Usage: "a valid ES builtin timestamp or custom format", + Destination: &conf.TimestampFormat, + Value: "strict_date_hour_minute", + }, + &cli.BoolFlag{ + Name: "or", + Usage: "logical operator (default: and)", + Destination: &conf.Or, + Aliases: []string{"O"}, + }, + }, + + Action: func(ctx context.Context, cmd *cli.Command) error { + if conf.Subhelp { + return showJsonPathHelp() + } + + args := cmd.Args() + + if args.Len() == 0 && !conf.All { + return errors.New("missing arguments: ") + } + + return es.DocDelete(conf, cmd.Args().Slice()) + }, + } +} diff --git a/cmd/search.go b/cmd/search.go index 7a95897..f54b814 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -26,14 +26,7 @@ import ( "github.com/urfave/cli/v3" ) -func Search(conf *cfg.Config) *cli.Command { - return &cli.Command{ - Name: "search", - Aliases: []string{"/"}, - Usage: "search within an index", - UsageText: `search [options] [<[field]pattern> ...] - - might be one of: +const SearchUsage = ` might be one of: =: Must match !=: Must not match @@ -52,7 +45,14 @@ https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#dat For timestamp formats refer to: https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/mapping-date-format -`, +` + +func Search(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "search", + Aliases: []string{"/"}, + Usage: "search within an index", + UsageText: "search [options] [<[field]pattern> ...]\n" + SearchUsage, Flags: []cli.Flag{ &cli.StringFlag{ diff --git a/pkg/es/doc.go b/pkg/es/doc.go index bbe94f8..18a85c2 100644 --- a/pkg/es/doc.go +++ b/pkg/es/doc.go @@ -21,9 +21,12 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "codeberg.org/scip/esctl/pkg/cfg" + "github.com/elastic/go-elasticsearch/v9/typedapi/core/deletebyquery" + "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" "github.com/tidwall/gjson" ) @@ -87,3 +90,43 @@ func DocShow(conf *cfg.Config, id string) error { return nil } + +func DocDelete(conf *cfg.Config, queries []string) error { + if len(queries) == 1 && strings.Contains(queries[0], "id=") { + id, _ := strings.CutPrefix(queries[0], "id=") + + _, err := conf.DefaultCluster.ES.Delete(conf.Index, id). + Header("content-type", "application/json"). + Header("accept", "application/json"). + Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to delete doc in index %s: %s", conf.Index, err) + } + + return nil + } + + req := &deletebyquery.Request{} + + if len(queries) == 0 && conf.All { + req.Query = esdsl.NewMatchAllQuery().QueryCaster() + } else { + queryCaster, err := prepareQuery(conf, queries) + if err != nil { + return err + } + + req.Query = queryCaster + } + + _, err := conf.DefaultCluster.ES.DeleteByQuery(conf.Index). + Request(req). + Header("content-type", "application/json"). + Header("accept", "application/json"). + Do(context.Background()) + if err != nil { + return fmt.Errorf("failed to delete docs in index %s: %s", conf.Index, err) + } + + return nil +} diff --git a/pkg/es/search.go b/pkg/es/search.go index 6164712..b82d849 100644 --- a/pkg/es/search.go +++ b/pkg/es/search.go @@ -44,24 +44,26 @@ Execute an ES search. additional filters can be given as -F key=value */ func Search(conf *cfg.Config, queries []string) error { - search := conf.DefaultCluster.ES.Search(). + searchEs := conf.DefaultCluster.ES.Search(). Index(conf.Index) - req, err := prepareQuery(conf, queries) + queryCaster, err := prepareQuery(conf, queries) if err != nil { return err } - search.Request(req) + req := &search.Request{Query: queryCaster} + + searchEs.Request(req) switch conf.Tail { case true: - return searchTail(conf, search) + return searchTail(conf, searchEs) case false: if conf.To > MAXPAGE { return searchPit(conf, req) } else { - return searchOnce(conf, search) + return searchOnce(conf, searchEs) } } diff --git a/pkg/es/search_filter.go b/pkg/es/search_filter.go index 986739b..8cdb895 100644 --- a/pkg/es/search_filter.go +++ b/pkg/es/search_filter.go @@ -23,7 +23,6 @@ import ( "strings" "codeberg.org/scip/esctl/pkg/cfg" - "github.com/elastic/go-elasticsearch/v9/typedapi/core/search" "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" "github.com/elastic/go-elasticsearch/v9/typedapi/types" "github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/operator" @@ -119,7 +118,7 @@ func mkMatchQueries(queries []string, op operator.Operator) ([]types.QueryVarian } // Prepare user search query and turn it into a proper search request -func prepareQuery(conf *cfg.Config, queries []string) (*search.Request, error) { +func prepareQuery(conf *cfg.Config, queries []string) (*types.Query, error) { // logical operator for simple and multimatch queries op := operator.Operator{Name: "AND"} if conf.Or { @@ -170,10 +169,7 @@ func prepareQuery(conf *cfg.Config, queries []string) (*search.Request, error) { query.Filter(filters...) } - // this being fed into ES.Serach().Req() - return &search.Request{ - Query: query.QueryCaster(), - }, nil + return query.QueryCaster(), nil } // Build a slice of filters, to be fed into query.Filter() including static ones