2026-04-21 15:05:15 +02:00
|
|
|
/*
|
|
|
|
|
Copyright © 2026 Thomas von Dein
|
|
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
package es
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-07-08 14:58:07 +02:00
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
2026-04-22 13:38:02 +02:00
|
|
|
"fmt"
|
2026-04-21 15:05:15 +02:00
|
|
|
"log/slog"
|
2026-05-19 13:58:08 +02:00
|
|
|
"regexp"
|
2026-06-03 10:14:41 +02:00
|
|
|
"slices"
|
2026-04-29 13:44:21 +02:00
|
|
|
"strconv"
|
2026-05-12 18:28:57 +02:00
|
|
|
"strings"
|
2026-04-29 13:44:21 +02:00
|
|
|
"time"
|
2026-04-21 15:05:15 +02:00
|
|
|
|
|
|
|
|
"codeberg.org/scip/esctl/pkg/cfg"
|
2026-05-20 13:06:39 +02:00
|
|
|
"codeberg.org/scip/esctl/pkg/printer"
|
2026-07-15 23:47:19 +02:00
|
|
|
"codeberg.org/scip/mapmap"
|
2026-07-08 14:58:07 +02:00
|
|
|
"github.com/charmbracelet/lipgloss"
|
2026-05-19 13:58:08 +02:00
|
|
|
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
|
2026-04-29 13:44:21 +02:00
|
|
|
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
|
2026-07-15 23:47:19 +02:00
|
|
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
|
2026-04-22 13:38:02 +02:00
|
|
|
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus"
|
2026-04-21 15:05:15 +02:00
|
|
|
)
|
|
|
|
|
|
2026-04-29 13:44:21 +02:00
|
|
|
// used for completion
|
|
|
|
|
func IndexNames(conf *cfg.Config) ([]string, error) {
|
2026-06-24 09:25:50 +02:00
|
|
|
res, err := conf.DefaultCluster.ES().Cat.Indices().
|
2026-04-29 13:44:21 +02:00
|
|
|
Do(context.Background())
|
|
|
|
|
if err != nil {
|
2026-07-16 23:21:45 +02:00
|
|
|
return nil, fmt.Errorf("failed to get indices: %w", esErrorString(err))
|
2026-04-29 13:44:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
indices := make([]string, len(res))
|
|
|
|
|
for idx, index := range res {
|
|
|
|
|
indices[idx] = *index.Index
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return indices, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 13:58:08 +02:00
|
|
|
func filterIndices(conf *cfg.Config, list indices.Response) indices.Response {
|
2026-07-15 23:47:19 +02:00
|
|
|
var filter *regexp.Regexp
|
2026-06-26 11:32:53 +02:00
|
|
|
|
2026-07-15 23:47:19 +02:00
|
|
|
if len(conf.Filter) > 0 {
|
|
|
|
|
filter = regexp.MustCompile(conf.Filter[0])
|
|
|
|
|
}
|
2026-06-26 11:32:53 +02:00
|
|
|
|
2026-07-15 23:47:19 +02:00
|
|
|
return mapmap.NewSlicer(list).MapSliceValuesImmutable(func(index types.IndicesRecord) bool {
|
|
|
|
|
if !conf.Partials && strings.HasPrefix(*index.Index, "partial-") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-05-19 13:58:08 +02:00
|
|
|
|
2026-07-15 23:47:19 +02:00
|
|
|
if !conf.Hidden && strings.HasPrefix(*index.Index, ".") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-05-19 13:58:08 +02:00
|
|
|
|
2026-07-15 23:47:19 +02:00
|
|
|
if len(conf.Filter) > 0 {
|
|
|
|
|
if !filter.MatchString(*index.Index) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-05-19 13:58:08 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-15 23:47:19 +02:00
|
|
|
return true
|
|
|
|
|
})
|
2026-05-19 13:58:08 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-21 15:05:15 +02:00
|
|
|
func IndexList(conf *cfg.Config) error {
|
2026-06-24 09:25:50 +02:00
|
|
|
cat := conf.DefaultCluster.ES().Cat.Indices()
|
2026-04-22 13:38:02 +02:00
|
|
|
|
|
|
|
|
if conf.Failed {
|
|
|
|
|
cat = cat.Health(healthstatus.Red)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res, err := cat.Do(context.Background())
|
2026-04-21 15:05:15 +02:00
|
|
|
if err != nil {
|
2026-07-16 23:21:45 +02:00
|
|
|
return fmt.Errorf("failed to get indices: %w", esErrorString(err))
|
2026-04-21 15:05:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-16 23:21:45 +02:00
|
|
|
slog.Debug("ES result", "indices", res)
|
2026-04-21 15:05:15 +02:00
|
|
|
|
2026-05-19 13:58:08 +02:00
|
|
|
list := filterIndices(conf, res)
|
|
|
|
|
|
|
|
|
|
size := len(list)
|
2026-04-21 15:05:15 +02:00
|
|
|
|
|
|
|
|
if conf.MaxItems > 0 {
|
|
|
|
|
if size > conf.MaxItems {
|
|
|
|
|
size = conf.MaxItems
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 21:53:47 +02:00
|
|
|
table := printer.NewTable(conf).WithSize(3, size).
|
|
|
|
|
WithHeaders("name", "size", "docscount")
|
2026-04-21 15:05:15 +02:00
|
|
|
|
2026-05-19 13:58:08 +02:00
|
|
|
for idx, index := range list {
|
2026-05-20 13:06:39 +02:00
|
|
|
name := printer.Colorize(conf, *index.Health, *index.Index)
|
2026-04-21 15:05:15 +02:00
|
|
|
|
2026-07-07 07:29:03 +02:00
|
|
|
table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
|
2026-04-21 15:05:15 +02:00
|
|
|
|
|
|
|
|
if idx == size-1 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table.Sort()
|
2026-07-07 23:46:43 +02:00
|
|
|
|
2026-06-15 15:18:21 +02:00
|
|
|
return table.Print()
|
2026-04-21 15:05:15 +02:00
|
|
|
}
|
2026-04-22 13:38:02 +02:00
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
func IndexShow(conf *cfg.Config, indexpattern string) error {
|
2026-06-24 09:25:50 +02:00
|
|
|
res, err := conf.DefaultCluster.ES().Indices.Get(indexpattern).
|
2026-04-27 13:05:30 +02:00
|
|
|
Do(context.Background())
|
2026-04-22 13:38:02 +02:00
|
|
|
if err != nil {
|
2026-07-07 10:41:34 +02:00
|
|
|
return fmt.Errorf("failed to get index: %w", esErrorString(err))
|
2026-04-22 13:38:02 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-25 07:52:54 +02:00
|
|
|
slog.Debug("index show", "index", res)
|
|
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
for name, index := range res {
|
|
|
|
|
fields := make([]string, len(index.Mappings.Properties))
|
2026-07-07 23:46:43 +02:00
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
idx := 0
|
|
|
|
|
for field := range index.Mappings.Properties {
|
|
|
|
|
fields[idx] = field
|
|
|
|
|
idx++
|
|
|
|
|
}
|
2026-04-22 13:38:02 +02:00
|
|
|
|
2026-08-09 21:53:47 +02:00
|
|
|
table := printer.NewTable(conf).WithSize(2, 7).
|
|
|
|
|
WithHeaders("index property", "value")
|
2026-05-22 13:50:17 +02:00
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ts = 0
|
|
|
|
|
}
|
2026-04-29 13:44:21 +02:00
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
created := time.Unix(ts/1000, 0)
|
2026-04-29 13:44:21 +02:00
|
|
|
|
2026-07-07 07:29:03 +02:00
|
|
|
table.Entries = [][]any{
|
2026-06-03 13:07:11 +02:00
|
|
|
{"name", name},
|
|
|
|
|
{"replicas", *index.Settings.Index.NumberOfReplicas},
|
|
|
|
|
{"shards", *index.Settings.Index.NumberOfShards},
|
2026-07-07 07:29:03 +02:00
|
|
|
{"created", created},
|
2026-06-03 13:07:11 +02:00
|
|
|
{"uuid", *index.Settings.Index.Uuid},
|
2026-06-25 07:52:54 +02:00
|
|
|
{"version", *index.Settings.Index.Version.Created},
|
2026-07-07 07:29:03 +02:00
|
|
|
{"fields", fields},
|
2026-06-03 13:07:11 +02:00
|
|
|
}
|
2026-04-29 13:44:21 +02:00
|
|
|
|
2026-06-25 07:52:54 +02:00
|
|
|
if index.Settings.Index.Lifecycle != nil {
|
2026-07-07 07:29:03 +02:00
|
|
|
table.Entries = append(table.Entries, [][]any{
|
2026-06-25 07:52:54 +02:00
|
|
|
{"ilm policy", *index.Settings.Index.Lifecycle.Name},
|
|
|
|
|
{"ilm rollover alias", *index.Settings.Index.Lifecycle.RolloverAlias},
|
|
|
|
|
}...)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
if err := table.Print(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-04-29 13:44:21 +02:00
|
|
|
|
2026-06-03 13:07:11 +02:00
|
|
|
fmt.Println()
|
2026-05-07 15:01:18 +02:00
|
|
|
}
|
2026-04-29 13:44:21 +02:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 18:28:57 +02:00
|
|
|
func IndexCreate(conf *cfg.Config, index string, mappings []string) error {
|
2026-04-29 13:44:21 +02:00
|
|
|
settings := esdsl.NewIndexSettings()
|
|
|
|
|
|
2026-06-24 09:25:50 +02:00
|
|
|
create := conf.DefaultCluster.ES().Indices.Create(index)
|
2026-04-29 13:44:21 +02:00
|
|
|
|
|
|
|
|
if conf.Wait {
|
|
|
|
|
create.WaitForActiveShards("all")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if conf.Shards > 0 {
|
2026-06-25 07:52:54 +02:00
|
|
|
settings.NumberOfShards(strconv.Itoa(conf.Shards))
|
2026-04-29 13:44:21 +02:00
|
|
|
}
|
2026-07-07 23:46:43 +02:00
|
|
|
|
2026-04-29 13:44:21 +02:00
|
|
|
if conf.Replicas > 0 {
|
2026-06-25 07:52:54 +02:00
|
|
|
settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
|
2026-04-29 13:44:21 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-12 18:28:57 +02:00
|
|
|
if len(mappings) > 0 {
|
|
|
|
|
maps := esdsl.NewTypeMapping()
|
|
|
|
|
|
|
|
|
|
for _, mapping := range mappings {
|
|
|
|
|
parts := strings.Split(mapping, ":")
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
|
return fmt.Errorf("invalid mapping %s, expect <name:type> (type: integer, text, date, keyword)", mapping)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch parts[1] {
|
|
|
|
|
case "text":
|
|
|
|
|
maps.AddProperty(parts[0], esdsl.NewTextProperty())
|
|
|
|
|
case "integer":
|
|
|
|
|
maps.AddProperty(parts[0], esdsl.NewIntegerNumberProperty())
|
|
|
|
|
case "date":
|
|
|
|
|
maps.AddProperty(parts[0], esdsl.NewDateProperty())
|
|
|
|
|
case "keyword":
|
|
|
|
|
maps.AddProperty(parts[0], esdsl.NewKeywordProperty())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create.Mappings(maps)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 07:52:54 +02:00
|
|
|
if conf.Policy != "" {
|
|
|
|
|
settings.Lifecycle(
|
|
|
|
|
esdsl.NewIndexSettingsLifecycle().
|
|
|
|
|
Name(conf.Policy).
|
|
|
|
|
RolloverAlias(index))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 13:44:21 +02:00
|
|
|
_, err := create.Settings(settings).
|
|
|
|
|
Do(context.Background())
|
|
|
|
|
if err != nil {
|
2026-07-07 10:41:34 +02:00
|
|
|
return fmt.Errorf("failed to create index: %w", esErrorString(err))
|
2026-04-29 13:44:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func IndexDelete(conf *cfg.Config, index string) error {
|
2026-06-24 09:25:50 +02:00
|
|
|
_, err := conf.DefaultCluster.ES().Indices.Delete(index).
|
2026-04-29 13:44:21 +02:00
|
|
|
Do(context.Background())
|
|
|
|
|
if err != nil {
|
2026-07-07 10:41:34 +02:00
|
|
|
return fmt.Errorf("failed to delete index: %w", esErrorString(err))
|
2026-04-29 13:44:21 +02:00
|
|
|
}
|
2026-04-22 13:38:02 +02:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-13 13:51:09 +02:00
|
|
|
|
|
|
|
|
func IndexClose(conf *cfg.Config, index string) error {
|
2026-06-24 09:25:50 +02:00
|
|
|
_, err := conf.DefaultCluster.ES().Indices.Close(index).
|
|
|
|
|
Do(context.Background())
|
2026-05-13 13:51:09 +02:00
|
|
|
if err != nil {
|
2026-07-07 10:41:34 +02:00
|
|
|
return fmt.Errorf("failed to close index: %w", esErrorString(err))
|
2026-05-13 13:51:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-05-18 13:58:08 +02:00
|
|
|
|
2026-06-01 12:30:41 +02:00
|
|
|
func IndexFields(conf *cfg.Config, index string) error {
|
2026-06-24 09:25:50 +02:00
|
|
|
res, err := conf.DefaultCluster.ES().FieldCaps().
|
2026-06-01 12:30:41 +02:00
|
|
|
Index(index).
|
|
|
|
|
Fields("*").
|
|
|
|
|
Do(context.Background())
|
|
|
|
|
if err != nil {
|
2026-07-07 10:41:34 +02:00
|
|
|
return fmt.Errorf("failed to retrieve field capabilties: %w", esErrorString(err))
|
2026-06-01 12:30:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 21:53:47 +02:00
|
|
|
table := printer.NewTable(conf).WithSize(5, 0).
|
|
|
|
|
WithHeaders("field", "type", "searchable", "aggretable", "metadata")
|
2026-06-01 12:30:41 +02:00
|
|
|
|
|
|
|
|
idx := 0
|
2026-07-07 23:46:43 +02:00
|
|
|
|
2026-06-01 12:30:41 +02:00
|
|
|
for name, field := range res.Fields {
|
|
|
|
|
for fieldtype, caps := range field {
|
|
|
|
|
// fields only have 1 type, so this one is it
|
2026-06-03 10:14:41 +02:00
|
|
|
switch {
|
|
|
|
|
case conf.Searchable && !caps.Searchable:
|
|
|
|
|
continue
|
|
|
|
|
case conf.Aggretable && !caps.Aggregatable:
|
|
|
|
|
continue
|
|
|
|
|
case len(conf.Filter) > 0 && !slices.Contains(conf.Filter, fieldtype):
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 07:29:03 +02:00
|
|
|
table.AddRow(name, fieldtype,
|
|
|
|
|
caps.Searchable,
|
|
|
|
|
caps.Aggregatable,
|
|
|
|
|
*caps.MetadataField)
|
|
|
|
|
|
2026-06-01 12:30:41 +02:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
idx++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table.Sort()
|
|
|
|
|
|
|
|
|
|
if err := table.Print(); err != nil {
|
|
|
|
|
return err
|
2026-05-19 13:58:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-07-08 14:58:07 +02:00
|
|
|
|
|
|
|
|
type Diskusage struct {
|
|
|
|
|
Total int64 `json:"total_in_bytes"`
|
|
|
|
|
Points int64 `json:"points_in_bytes"`
|
|
|
|
|
Norms int64 `json:"norms_in_bytes"`
|
|
|
|
|
TermVectors int64 `json:"term_vectors_in_bytes"`
|
|
|
|
|
KnnVectors int64 `json:"knn_vectors_in_bytes"`
|
|
|
|
|
BloomFilter int64 `json:"bloom_filter_in_bytes"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type IndexDiskUsage struct {
|
|
|
|
|
AllFields Diskusage `json:"all_fields"`
|
|
|
|
|
Fields map[string]Diskusage `json:"fields"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ResIndexDiskUsage map[string]IndexDiskUsage
|
|
|
|
|
|
|
|
|
|
func IndexDiskusage(conf *cfg.Config, index string) error {
|
|
|
|
|
var bold = lipgloss.NewStyle().Bold(true)
|
|
|
|
|
|
|
|
|
|
res, err := conf.DefaultCluster.ES().Indices.DiskUsage(index).
|
|
|
|
|
RunExpensiveTasks(true).
|
|
|
|
|
Do(context.Background())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to retrieve index disk usage: %w", esErrorString(err))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:00:21 +02:00
|
|
|
duRes := ResIndexDiskUsage{}
|
|
|
|
|
if err := json.Unmarshal(res, &duRes); err != nil {
|
2026-07-08 14:58:07 +02:00
|
|
|
return fmt.Errorf("failed to unmarshal disk usage response: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:00:21 +02:00
|
|
|
diskusage, exists := duRes[index]
|
2026-07-08 14:58:07 +02:00
|
|
|
if !exists {
|
|
|
|
|
return errors.New("no disk usage reported for index")
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 21:53:47 +02:00
|
|
|
table := printer.NewTable(conf).
|
2026-07-08 14:58:07 +02:00
|
|
|
WithHeaders("field", "bloom filter", "norms", "points", "term vectors", "knn vectors", "total")
|
|
|
|
|
|
2026-07-08 15:00:21 +02:00
|
|
|
for name, field := range diskusage.Fields {
|
2026-07-08 14:58:07 +02:00
|
|
|
if strings.HasPrefix(name, "_") || strings.HasSuffix(name, ".keyword") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
table.AddRow(
|
|
|
|
|
name,
|
|
|
|
|
printer.Bytes(field.BloomFilter),
|
|
|
|
|
printer.Bytes(field.Norms),
|
|
|
|
|
printer.Bytes(field.Points),
|
|
|
|
|
printer.Bytes(field.TermVectors),
|
|
|
|
|
printer.Bytes(field.KnnVectors),
|
|
|
|
|
printer.Bytes(field.Total),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 15:00:21 +02:00
|
|
|
all := diskusage.AllFields
|
2026-07-08 14:58:07 +02:00
|
|
|
|
|
|
|
|
table.Sort()
|
|
|
|
|
|
|
|
|
|
table.AddRowLate(
|
|
|
|
|
bold.Render("Summary"),
|
|
|
|
|
printer.Bytes(all.BloomFilter),
|
|
|
|
|
printer.Bytes(all.Norms),
|
|
|
|
|
printer.Bytes(all.Points),
|
|
|
|
|
printer.Bytes(all.TermVectors),
|
|
|
|
|
printer.Bytes(all.KnnVectors),
|
|
|
|
|
printer.Bytes(all.Total),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if err := table.Print(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|