mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-03 15:10:44 +02:00
abcfa53040
Drops `github.com/olivere/elastic/v7` (unmaintained) and replaces it
with a small in-house wrapper that speaks the Elasticsearch REST API
directly via `net/http`. The subset used by Gitea (`_cluster/health`,
`_bulk`, `_doc`, `_delete_by_query`, `_refresh`, `_search`, `HEAD`/`PUT`
index) is stable across the targeted servers, so no client library is
needed.
**Targets tested**
- Elasticsearch 7, 8, 9
- OpenSearch 1, 2, 3
**Why not `go-elasticsearch`?**
The official client enforces an `X-Elastic-Product` server-identity
check that OpenSearch deliberately fails, which would force shipping a
transport shim to defeat it. Going direct over `net/http` removes that
fight along with several MB of transitive deps (`elastic-transport-go`,
`go.opentelemetry.io/otel{,/metric,/trace}`, `auto/sdk`, `easyjson`,
`intern`, `logr`, `stdr`).
Replaces: #30755
Fixes: https://github.com/go-gitea/gitea/issues/30752
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
133 lines
3.8 KiB
Go
133 lines
3.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package elasticsearch
|
|
|
|
// MultiMatch types used by the call sites. See
|
|
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types
|
|
const (
|
|
MultiMatchTypeBestFields = "best_fields"
|
|
MultiMatchTypePhrasePrefix = "phrase_prefix"
|
|
)
|
|
|
|
// ToAnySlice converts []T to []any for variadic query args like TermsQuery.
|
|
func ToAnySlice[T any](s []T) []any {
|
|
out := make([]any, len(s))
|
|
for idx, v := range s {
|
|
out[idx] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Query is an Elasticsearch query DSL node. It marshals to the JSON
|
|
// object expected by the ES query API.
|
|
type Query interface {
|
|
querySource() map[string]any
|
|
}
|
|
|
|
type rawQuery map[string]any
|
|
|
|
func (q rawQuery) querySource() map[string]any { return q }
|
|
|
|
// TermQuery matches documents whose `field` exactly equals `value`.
|
|
func TermQuery(field string, value any) Query {
|
|
return rawQuery{"term": map[string]any{field: value}}
|
|
}
|
|
|
|
// TermsQuery matches documents whose `field` equals any of `values`.
|
|
func TermsQuery(field string, values ...any) Query {
|
|
return rawQuery{"terms": map[string]any{field: values}}
|
|
}
|
|
|
|
// MatchQuery is a full-text match on a single field.
|
|
func MatchQuery(field string, value any) Query {
|
|
return rawQuery{"match": map[string]any{field: value}}
|
|
}
|
|
|
|
// MatchPhraseQuery matches the exact phrase on `field`.
|
|
func MatchPhraseQuery(field, value string) Query {
|
|
return rawQuery{"match_phrase": map[string]any{field: value}}
|
|
}
|
|
|
|
// MultiMatchQuery is the fluent builder for a multi_match query.
|
|
type MultiMatchQuery struct {
|
|
query any
|
|
fields []string
|
|
typ string
|
|
operator string
|
|
}
|
|
|
|
// NewMultiMatchQuery creates a multi_match query over the given fields.
|
|
func NewMultiMatchQuery(query any, fields ...string) *MultiMatchQuery {
|
|
return &MultiMatchQuery{query: query, fields: fields}
|
|
}
|
|
|
|
func (m *MultiMatchQuery) Type(t string) *MultiMatchQuery { m.typ = t; return m }
|
|
func (m *MultiMatchQuery) Operator(op string) *MultiMatchQuery { m.operator = op; return m }
|
|
|
|
func (m *MultiMatchQuery) querySource() map[string]any {
|
|
body := map[string]any{"query": m.query}
|
|
if len(m.fields) > 0 {
|
|
body["fields"] = m.fields
|
|
}
|
|
if m.typ != "" {
|
|
body["type"] = m.typ
|
|
}
|
|
if m.operator != "" {
|
|
body["operator"] = m.operator
|
|
}
|
|
return map[string]any{"multi_match": body}
|
|
}
|
|
|
|
// RangeQuery is the fluent builder for a range query.
|
|
type RangeQuery struct {
|
|
field string
|
|
body map[string]any
|
|
}
|
|
|
|
func NewRangeQuery(field string) *RangeQuery {
|
|
return &RangeQuery{field: field, body: map[string]any{}}
|
|
}
|
|
|
|
func (r *RangeQuery) Gte(v any) *RangeQuery { r.body["gte"] = v; return r }
|
|
func (r *RangeQuery) Lte(v any) *RangeQuery { r.body["lte"] = v; return r }
|
|
|
|
func (r *RangeQuery) querySource() map[string]any {
|
|
return map[string]any{"range": map[string]any{r.field: r.body}}
|
|
}
|
|
|
|
// BoolQuery is the fluent builder for a bool query.
|
|
type BoolQuery struct {
|
|
must []Query
|
|
should []Query
|
|
mustNot []Query
|
|
}
|
|
|
|
func NewBoolQuery() *BoolQuery { return &BoolQuery{} }
|
|
|
|
func (b *BoolQuery) Must(q ...Query) *BoolQuery { b.must = append(b.must, q...); return b }
|
|
func (b *BoolQuery) Should(q ...Query) *BoolQuery { b.should = append(b.should, q...); return b }
|
|
func (b *BoolQuery) MustNot(q ...Query) *BoolQuery { b.mustNot = append(b.mustNot, q...); return b }
|
|
|
|
func (b *BoolQuery) querySource() map[string]any {
|
|
body := map[string]any{}
|
|
if len(b.must) > 0 {
|
|
body["must"] = querySlice(b.must)
|
|
}
|
|
if len(b.should) > 0 {
|
|
body["should"] = querySlice(b.should)
|
|
}
|
|
if len(b.mustNot) > 0 {
|
|
body["must_not"] = querySlice(b.mustNot)
|
|
}
|
|
return map[string]any{"bool": body}
|
|
}
|
|
|
|
func querySlice(queries []Query) []map[string]any {
|
|
out := make([]map[string]any, len(queries))
|
|
for idx, q := range queries {
|
|
out[idx] = q.querySource()
|
|
}
|
|
return out
|
|
}
|