add doc show, enhance search, add jsonpath to search and doc show

This commit is contained in:
T. von Dein
2026-05-22 13:50:17 +02:00
parent 89e2498aad
commit 98a577f68a
10 changed files with 348 additions and 52 deletions

View File

@@ -19,10 +19,12 @@ package es
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"codeberg.org/scip/esctl/pkg/cfg"
"github.com/tidwall/gjson"
)
// create index with:
@@ -32,7 +34,7 @@ import (
// then add a doc:
//
// esctl doc add foo2 '{"id":"d8d8d","user":"scip"}'
func DocAdd(conf *cfg.Config, index, jsondoc string) error {
func DocAdd(conf *cfg.Config, jsondoc string) error {
data := map[string]any{}
err := json.Unmarshal([]byte(jsondoc), &data)
@@ -42,14 +44,46 @@ func DocAdd(conf *cfg.Config, index, jsondoc string) error {
now := fmt.Sprintf("%d", time.Now().Unix())
_, err = conf.DefaultCluster.ES.Create(index, now).
res, err := conf.DefaultCluster.ES.Create(conf.Index, now).
Document(data).
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to create new doc in index %s: %s", index, err)
return fmt.Errorf("failed to create new doc in index %s: %s", conf.Index, err)
}
fmt.Println(res.Id_)
return nil
}
func DocShow(conf *cfg.Config, id string) error {
res, err := conf.DefaultCluster.ES.Get(conf.Index, id).
Header("content-type", "application/json").
Header("accept", "application/json").
Do(context.Background())
if err != nil {
return fmt.Errorf("failed to retrieve doc in index %s: %s", conf.Index, err)
}
if !res.Found {
return errors.New("no document with that id found")
}
docjson := fmt.Sprintf(`{"id":%s, "index":"%s", "source":%s}`,
res.Id_,
res.Index_,
res.Source_)
if conf.Path != "" {
value := gjson.Get(docjson, conf.Path)
fmt.Println(value.String())
} else {
fmt.Println(docjson)
}
fmt.Println()
return nil
}

View File

@@ -129,6 +129,13 @@ func IndexShow(conf *cfg.Config, index string) error {
slog.Debug("ES result", "index", res)
fields := make([]string, len(res[index].Mappings.Properties))
idx := 0
for field := range res[index].Mappings.Properties {
fields[idx] = field
idx++
}
table := printer.NewTable(conf, 2, 5)
table.Addheaders("index property", "value")
@@ -145,6 +152,7 @@ func IndexShow(conf *cfg.Config, index string) error {
{"shards", *res[index].Settings.Index.NumberOfShards},
{"created", created.Format("2006-01-02 15:04:05")},
{"uuid", *res[index].Settings.Index.Uuid},
{"fields", strings.Join(fields, ",")},
}
if err := table.Print(); err != nil {

View File

@@ -20,12 +20,9 @@ import (
"context"
"fmt"
"log/slog"
"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/tidwall/gjson"
)
/*
@@ -35,40 +32,19 @@ Execute an ES search.
additional filters can be given as -F key=value
*/
func Search(conf *cfg.Config, queries []string) error {
query := esdsl.NewBoolQuery()
search := conf.DefaultCluster.ES.Search().
Index(conf.Index)
for _, q := range queries {
parts := strings.Split(q, "=")
if len(parts) != 2 {
return fmt.Errorf("search queries must be in the form field=pattern")
if len(queries) > 0 {
req, err := prepareQuery(conf, queries)
if err != nil {
return err
}
query.Must(esdsl.NewMatchQuery(parts[0], parts[1]))
search.Request(req)
}
filters := make([]types.QueryVariant, len(conf.Filter))
for idx, filter := range conf.Filter {
parts := strings.Split(filter, "=")
if len(parts) != 2 {
return fmt.Errorf("invalid filter spec: %s, expecting key=value", filter)
}
filters[idx] = esdsl.NewTermQuery(parts[0], esdsl.NewFieldValue().String(parts[1]))
}
if len(filters) > 0 {
query.Filter(filters...)
}
res, err := conf.DefaultCluster.ES.Search().
Index(conf.Index).
Request(&search.Request{
Query: query.QueryCaster(),
From: &conf.From,
Size: &conf.To,
}).
Do(context.Background())
res, err := search.Do(context.Background())
if err != nil {
return fmt.Errorf("failed to run search (esdsl): %s", err)
}
@@ -76,7 +52,18 @@ func Search(conf *cfg.Config, queries []string) error {
slog.Debug("ES result", "search", res)
for _, hit := range res.Hits.Hits {
fmt.Printf("%s\n", hit.Source_)
docjson := fmt.Sprintf(`{"id":%s, "score":%0.4f, "index":"%s", "source":%s}`,
*hit.Id_,
*hit.Score_,
hit.Index_,
hit.Source_)
if conf.Path != "" {
value := gjson.Get(docjson, conf.Path)
fmt.Println(value.String())
} else {
fmt.Println(docjson)
}
}
return nil

146
pkg/es/search_filter.go Normal file
View File

@@ -0,0 +1,146 @@
/*
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 (
"fmt"
"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"
)
const (
Fmust = iota
Fmustnot
Fshould
)
// hold filter configuration, can be used multiple times through a search
type filter struct {
term, filter string // message=foo
mterm []string // multi_match
criteria int // must(=), mustnot(!=), should(?)
multi bool // message,title=foo => filter:foo, multi: []string{"message","title"}
}
// build a new filter object
func NewFilter(query string) (*filter, error) {
var separator string
var criteria int // we use the constants on top for this
switch {
case strings.Contains(query, "!="):
criteria = Fmustnot
separator = "!="
case strings.Contains(query, "?"):
criteria = Fshould
separator = "?"
default:
criteria = Fmust
separator = "="
}
part := strings.Split(query, separator)
if len(part) != 2 {
return nil, fmt.Errorf("search queries must be in the form field<sep>pattern where <sep> must be one of: =, !=, ?")
}
f := &filter{term: part[0], filter: part[1], criteria: criteria}
if strings.Contains(part[0], ",") {
// a MultiMatchQuery, match across multiple fields at once
multi := strings.Split(part[0], ",")
f.multi = true
f.mterm = multi
}
return f, nil
}
// prepare q user search query and turn it into a proper search request
func prepareQuery(conf *cfg.Config, queries []string) (*search.Request, error) {
if len(queries) == 0 {
// nothing given, just return all docs, if any
return &search.Request{
Query: esdsl.NewMatchAllQuery().QueryCaster(),
From: &conf.From,
Size: &conf.To,
}, nil
}
if len(queries) == 1 && !strings.ContainsAny(queries[0], "!=?") {
// a general query w/o fields, search across all fields
return &search.Request{
Query: esdsl.NewSimpleQueryStringQuery(queries[0]).QueryCaster(),
From: &conf.From,
Size: &conf.To,
}, nil
}
// complex query, form a proper query struct
query := esdsl.NewBoolQuery()
for _, q := range queries {
filter, err := NewFilter(q)
if err != nil {
return nil, err
}
// by default we match on a single field
var match types.QueryVariant = esdsl.NewMatchQuery(filter.term, filter.filter)
if filter.multi {
// ok, match across multiple given fields
match = esdsl.NewMultiMatchQuery(filter.filter).Fields(filter.mterm...)
}
// apply logic
switch filter.criteria {
case Fmustnot:
query.MustNot(match)
case Fmust:
query.Must(match)
case Fshould:
query.Should(match)
}
}
// there might be boolean filters as well
filters := make([]types.QueryVariant, len(conf.Filter))
for idx, filter := range conf.Filter {
parts := strings.Split(filter, "=")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid filter spec: %s, expecting key=value", filter)
}
filters[idx] = esdsl.NewTermQuery(parts[0], esdsl.NewFieldValue().String(parts[1]))
}
if len(filters) > 0 {
query.Filter(filters...)
}
return &search.Request{
Query: query.QueryCaster(),
From: &conf.From,
Size: &conf.To,
}, nil
}