Files
es-demo/operations/index/search.go

188 lines
4.3 KiB
Go

// Package index provides index-level operations for OpenSearch.
package index
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"es-demo/client"
"github.com/opensearch-project/opensearch-go/v2/opensearchapi"
)
// SearchQuery represents a search query configuration.
type SearchQuery struct {
// Query is the search query DSL
Query map[string]any `json:"query,omitempty"`
// Size is the maximum number of hits to return (default: 10)
Size int `json:"size,omitempty"`
// From is the starting offset (for pagination)
From int `json:"from,omitempty"`
// Sort defines the sort order
Sort []map[string]any `json:"sort,omitempty"`
// Source defines which fields to return
Source any `json:"_source,omitempty"`
}
// SearchResult represents search results.
type SearchResult struct {
Took int64 `json:"took"`
Hits struct {
Total struct {
Value int64 `json:"value"`
Relation string `json:"relation"`
} `json:"total"`
MaxScore *float64 `json:"max_score"`
Hits []Hit `json:"hits"`
} `json:"hits"`
}
// Hit represents a single search result hit.
type Hit struct {
Index string `json:"_index"`
ID string `json:"_id"`
Score *float64 `json:"_score"`
Source map[string]any `json:"_source"`
}
// Search performs a search query on an index.
func Search(ctx context.Context, c *client.Client, indexName string, query *SearchQuery) (*SearchResult, error) {
if indexName == "" {
return nil, ErrInvalidIndexName
}
if query == nil {
query = &SearchQuery{}
}
// Set defaults
if query.Size == 0 {
query.Size = 10
}
data, err := json.Marshal(query)
if err != nil {
return nil, fmt.Errorf("failed to marshal search query: %w", err)
}
req := opensearchapi.SearchRequest{
Index: []string{indexName},
Body: bytes.NewReader(data),
}
res, err := req.Do(ctx, c.GetClient())
if err != nil {
return nil, fmt.Errorf("failed to execute search request: %w", err)
}
defer func() {
if closeErr := res.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
if res.IsError() {
bodyBytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("search failed with status %s: %s", res.Status(), string(bodyBytes))
}
var result SearchResult
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode search result: %w", err)
}
return &result, nil
}
// MatchQuery creates a match query for full-text search.
func MatchQuery(field string, value any) map[string]any {
return map[string]any{
"match": map[string]any{
field: value,
},
}
}
// TermQuery creates a term query for exact matching.
func TermQuery(field string, value any) map[string]any {
return map[string]any{
"term": map[string]any{
field: value,
},
}
}
// RangeQuery creates a range query.
func RangeQuery(field string, gte, lte any) map[string]any {
rangeMap := make(map[string]any)
if gte != nil {
rangeMap["gte"] = gte
}
if lte != nil {
rangeMap["lte"] = lte
}
return map[string]any{
"range": map[string]any{
field: rangeMap,
},
}
}
// BoolQuery creates a bool query for combining multiple queries.
type BoolQuery struct {
Must []map[string]any `json:"must,omitempty"`
Should []map[string]any `json:"should,omitempty"`
MustNot []map[string]any `json:"must_not,omitempty"`
Filter []map[string]any `json:"filter,omitempty"`
}
// ToBoolQuery converts BoolQuery to query DSL.
func (b *BoolQuery) ToBoolQuery() map[string]any {
return map[string]any{
"bool": b,
}
}
// MatchAllQuery creates a match_all query.
func MatchAllQuery() map[string]any {
return map[string]any{
"match_all": map[string]any{},
}
}
// MultiMatchQuery creates a multi_match query for searching across multiple fields.
func MultiMatchQuery(value any, fields ...string) map[string]any {
return map[string]any{
"multi_match": map[string]any{
"query": value,
"fields": fields,
},
}
}
// WildcardQuery creates a wildcard query.
func WildcardQuery(field string, value string) map[string]any {
return map[string]any{
"wildcard": map[string]any{
field: value,
},
}
}
// PrefixQuery creates a prefix query.
func PrefixQuery(field string, value string) map[string]any {
return map[string]any{
"prefix": map[string]any{
field: value,
},
}
}