/*
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 .
*/
package es
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"regexp"
"slices"
"strconv"
"strings"
"time"
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"codeberg.org/scip/mapmap"
"github.com/charmbracelet/lipgloss"
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
"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/healthstatus"
)
// used for completion
func IndexNames(conf *cfg.Config) ([]string, error) {
res, err := conf.DefaultCluster.ES().Cat.Indices().
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get indicies: %w", esErrorString(err))
}
indices := make([]string, len(res))
for idx, index := range res {
indices[idx] = *index.Index
}
return indices, nil
}
func filterIndices(conf *cfg.Config, list indices.Response) indices.Response {
var filter *regexp.Regexp
if len(conf.Filter) > 0 {
filter = regexp.MustCompile(conf.Filter[0])
}
return mapmap.NewSlicer(list).MapSliceValuesImmutable(func(index types.IndicesRecord) bool {
fmt.Println(*index.Index)
if strings.HasPrefix(*index.Index, ".ds-") {
// ignore data stream backing indicies
return false
}
if !conf.Partials && strings.HasPrefix(*index.Index, "partial-") {
return false
}
if !conf.Hidden && strings.HasPrefix(*index.Index, ".") {
return false
}
if len(conf.Filter) > 0 {
if !filter.MatchString(*index.Index) {
return false
}
}
return true
})
}
func IndexList(conf *cfg.Config) error {
cat := conf.DefaultCluster.ES().Cat.Indices()
if conf.Failed {
cat = cat.Health(healthstatus.Red)
}
res, err := cat.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get indicies: %w", esErrorString(err))
}
slog.Debug("ES result", "indicies", res)
list := filterIndices(conf, res)
size := len(list)
if conf.MaxItems > 0 {
if size > conf.MaxItems {
size = conf.MaxItems
}
}
table := printer.NewTable(conf, 3, size)
table.Addheaders("name", "size", "docscount")
for idx, index := range list {
name := printer.Colorize(conf, *index.Health, *index.Index)
table.Entries[idx] = []any{name, *index.DatasetSize, *index.DocsCount}
if idx == size-1 {
break
}
}
table.Sort()
return table.Print()
}
func IndexShow(conf *cfg.Config, indexpattern string) error {
res, err := conf.DefaultCluster.ES().Indices.Get(indexpattern).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to get index: %w", esErrorString(err))
}
slog.Debug("index show", "index", res)
for name, index := range res {
fields := make([]string, len(index.Mappings.Properties))
idx := 0
for field := range index.Mappings.Properties {
fields[idx] = field
idx++
}
table := printer.NewTable(conf, 2, 7)
table.Addheaders("index property", "value")
ts, err := strconv.ParseInt(index.Settings.Index.CreationDate.(string), 10, 64)
if err != nil {
ts = 0
}
created := time.Unix(ts/1000, 0)
table.Entries = [][]any{
{"name", name},
{"replicas", *index.Settings.Index.NumberOfReplicas},
{"shards", *index.Settings.Index.NumberOfShards},
{"created", created},
{"uuid", *index.Settings.Index.Uuid},
{"version", *index.Settings.Index.Version.Created},
{"fields", fields},
}
if index.Settings.Index.Lifecycle != nil {
table.Entries = append(table.Entries, [][]any{
{"ilm policy", *index.Settings.Index.Lifecycle.Name},
{"ilm rollover alias", *index.Settings.Index.Lifecycle.RolloverAlias},
}...)
}
if err := table.Print(); err != nil {
return err
}
fmt.Println()
}
return nil
}
func IndexCreate(conf *cfg.Config, index string, mappings []string) error {
settings := esdsl.NewIndexSettings()
create := conf.DefaultCluster.ES().Indices.Create(index)
if conf.Wait {
create.WaitForActiveShards("all")
}
if conf.Shards > 0 {
settings.NumberOfShards(strconv.Itoa(conf.Shards))
}
if conf.Replicas > 0 {
settings.NumberOfReplicas(strconv.Itoa(conf.Replicas))
}
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 (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)
}
if conf.Policy != "" {
settings.Lifecycle(
esdsl.NewIndexSettingsLifecycle().
Name(conf.Policy).
RolloverAlias(index))
}
_, err := create.Settings(settings).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to create index: %w", esErrorString(err))
}
return nil
}
func IndexDelete(conf *cfg.Config, index string) error {
_, err := conf.DefaultCluster.ES().Indices.Delete(index).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to delete index: %w", esErrorString(err))
}
return nil
}
func IndexClose(conf *cfg.Config, index string) error {
_, err := conf.DefaultCluster.ES().Indices.Close(index).
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to close index: %w", esErrorString(err))
}
return nil
}
func IndexFields(conf *cfg.Config, index string) error {
res, err := conf.DefaultCluster.ES().FieldCaps().
Index(index).
Fields("*").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to retrieve field capabilties: %w", esErrorString(err))
}
table := printer.NewTable(conf, 5, 0)
table.Addheaders("field", "type", "searchable", "aggretable", "metadata")
idx := 0
for name, field := range res.Fields {
for fieldtype, caps := range field {
// fields only have 1 type, so this one is it
switch {
case conf.Searchable && !caps.Searchable:
continue
case conf.Aggretable && !caps.Aggregatable:
continue
case len(conf.Filter) > 0 && !slices.Contains(conf.Filter, fieldtype):
continue
}
table.AddRow(name, fieldtype,
caps.Searchable,
caps.Aggregatable,
*caps.MetadataField)
break
}
idx++
}
table.Sort()
if err := table.Print(); err != nil {
return err
}
return nil
}
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))
}
duRes := ResIndexDiskUsage{}
if err := json.Unmarshal(res, &duRes); err != nil {
return fmt.Errorf("failed to unmarshal disk usage response: %w", err)
}
diskusage, exists := duRes[index]
if !exists {
return errors.New("no disk usage reported for index")
}
table := printer.NewTableEmpty(conf).
WithHeaders("field", "bloom filter", "norms", "points", "term vectors", "knn vectors", "total")
for name, field := range diskusage.Fields {
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),
)
}
all := diskusage.AllFields
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
}