579 lines
14 KiB
Go
579 lines
14 KiB
Go
package elastic
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"go-common/library/ecode"
|
|
httpx "go-common/library/net/http/blademaster"
|
|
timex "go-common/library/time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
// OrderAsc order ascend
|
|
OrderAsc = "asc"
|
|
// OrderDesc order descend
|
|
OrderDesc = "desc"
|
|
|
|
// RangeScopeLoRo left open & right open
|
|
RangeScopeLoRo rangeScope = "( )"
|
|
// RangeScopeLoRc left open & right close
|
|
RangeScopeLoRc rangeScope = "( ]"
|
|
// RangeScopeLcRo left close & right open
|
|
RangeScopeLcRo rangeScope = "[ )"
|
|
// RangeScopeLcRc lect close & right close
|
|
RangeScopeLcRc rangeScope = "[ ]"
|
|
|
|
// NotTypeEq not type eq
|
|
NotTypeEq notType = "eq"
|
|
// NotTypeIn not type in
|
|
NotTypeIn notType = "in"
|
|
// NotTypeRange not type range
|
|
NotTypeRange notType = "range"
|
|
|
|
// LikeLevelHigh wildcard keyword
|
|
LikeLevelHigh likeLevel = "high"
|
|
// LikeLevelMiddle ngram(1,2)
|
|
LikeLevelMiddle likeLevel = "middle"
|
|
// LikeLevelLow match split word
|
|
LikeLevelLow likeLevel = "low"
|
|
|
|
// IndexTypeYear index by year
|
|
IndexTypeYear indexType = "year"
|
|
// IndexTypeMonth index by month
|
|
IndexTypeMonth indexType = "month"
|
|
// IndexTypeWeek index by week
|
|
IndexTypeWeek indexType = "week"
|
|
// IndexTypeDay index by day
|
|
IndexTypeDay indexType = "day"
|
|
|
|
// EnhancedModeGroupBy group by mode
|
|
EnhancedModeGroupBy enhancedType = "group_by"
|
|
// EnhancedModeDistinct distinct mode
|
|
EnhancedModeDistinct enhancedType = "distinct"
|
|
// EnhancedModeSum sum mode
|
|
EnhancedModeSum enhancedType = "sum"
|
|
)
|
|
|
|
type (
|
|
notType string
|
|
rangeScope string
|
|
likeLevel string
|
|
indexType string
|
|
enhancedType string
|
|
)
|
|
|
|
var (
|
|
_defaultHost = "http://manager.bilibili.co"
|
|
_pathQuery = "/x/admin/search/query"
|
|
_pathUpsert = "/x/admin/search/upsert"
|
|
|
|
_defaultHTTPClient = &httpx.ClientConfig{
|
|
App: &httpx.App{
|
|
Key: "3c4e41f926e51656",
|
|
Secret: "26a2095b60c24154521d24ae62b885bb",
|
|
},
|
|
Dial: timex.Duration(time.Second),
|
|
Timeout: timex.Duration(time.Second),
|
|
}
|
|
|
|
// for index by week
|
|
weeks = map[int]string{0: "0107", 1: "0815", 2: "1623", 3: "2431"}
|
|
)
|
|
|
|
// Config Elastic config
|
|
type Config struct {
|
|
Host string
|
|
HTTPClient *httpx.ClientConfig
|
|
}
|
|
|
|
// response query elastic response
|
|
type response struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
type query struct {
|
|
Fields []string `json:"fields"`
|
|
From string `json:"from"`
|
|
OrderScoreFirst bool `json:"order_score_first"`
|
|
OrderRandomSeed string `json:"order_random_seed"`
|
|
Highlight bool `json:"highlight"`
|
|
Pn int `json:"pn"`
|
|
Ps int `json:"ps"`
|
|
Order []map[string]string `json:"order,omitempty"`
|
|
Where where `json:"where,omitempty"`
|
|
}
|
|
|
|
func (q *query) string() (string, error) {
|
|
var (
|
|
sli []string
|
|
m = make(map[string]bool)
|
|
)
|
|
for _, i := range strings.Split(q.From, ",") {
|
|
if m[i] {
|
|
continue
|
|
}
|
|
m[i] = true
|
|
sli = append(sli, i)
|
|
}
|
|
q.From = strings.Join(sli, ",")
|
|
bs, err := json.Marshal(q)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(bs), nil
|
|
}
|
|
|
|
type where struct {
|
|
GroupBy string `json:"group_by,omitempty"`
|
|
Like []whereLike `json:"like,omitempty"`
|
|
Eq map[string]interface{} `json:"eq,omitempty"`
|
|
Or map[string]interface{} `json:"or,omitempty"`
|
|
In map[string][]interface{} `json:"in,omitempty"`
|
|
Range map[string]string `json:"range,omitempty"`
|
|
Combo []*Combo `json:"combo,omitempty"`
|
|
Not map[notType]map[string]bool `json:"not,omitempty"`
|
|
Enhanced []interface{} `json:"enhanced,omitempty"`
|
|
}
|
|
|
|
type whereLike struct {
|
|
Fields []string `json:"kw_fields"`
|
|
Words []string `json:"kw"`
|
|
Or bool `json:"or"`
|
|
Level likeLevel `json:"level"`
|
|
}
|
|
|
|
// Combo mix eq & in & range
|
|
type Combo struct {
|
|
EQ []map[string]interface{} `json:"eq,omitempty"`
|
|
In []map[string][]interface{} `json:"in,omitempty"`
|
|
Range []map[string]string `json:"range,omitempty"`
|
|
NotEQ []map[string]interface{} `json:"not_eq,omitempty"`
|
|
NotIn []map[string][]interface{} `json:"not_in,omitempty"`
|
|
NotRange []map[string]string `json:"not_range,omitempty"`
|
|
Min struct {
|
|
EQ int `json:"eq,omitempty"`
|
|
In int `json:"in,omitempty"`
|
|
Range int `json:"range,omitempty"`
|
|
NotEQ int `json:"not_eq,omitempty"`
|
|
NotIn int `json:"not_in,omitempty"`
|
|
NotRange int `json:"not_range,omitempty"`
|
|
Min int `json:"min"`
|
|
} `json:"min"`
|
|
}
|
|
|
|
// ComboEQ .
|
|
func (cmb *Combo) ComboEQ(eq []map[string]interface{}) *Combo {
|
|
cmb.EQ = append(cmb.EQ, eq...)
|
|
return cmb
|
|
}
|
|
|
|
// ComboRange .
|
|
func (cmb *Combo) ComboRange(r []map[string]string) *Combo {
|
|
cmb.Range = append(cmb.Range, r...)
|
|
return cmb
|
|
}
|
|
|
|
// ComboIn .
|
|
func (cmb *Combo) ComboIn(in []map[string][]interface{}) *Combo {
|
|
cmb.In = append(cmb.In, in...)
|
|
return cmb
|
|
}
|
|
|
|
// ComboNotEQ .
|
|
func (cmb *Combo) ComboNotEQ(eq []map[string]interface{}) *Combo {
|
|
cmb.NotEQ = append(cmb.NotEQ, eq...)
|
|
return cmb
|
|
}
|
|
|
|
// ComboNotRange .
|
|
func (cmb *Combo) ComboNotRange(r []map[string]string) *Combo {
|
|
cmb.NotRange = append(cmb.NotRange, r...)
|
|
return cmb
|
|
}
|
|
|
|
// ComboNotIn .
|
|
func (cmb *Combo) ComboNotIn(in []map[string][]interface{}) *Combo {
|
|
cmb.NotIn = append(cmb.NotIn, in...)
|
|
return cmb
|
|
}
|
|
|
|
// MinEQ .
|
|
func (cmb *Combo) MinEQ(min int) *Combo {
|
|
cmb.Min.EQ = min
|
|
return cmb
|
|
}
|
|
|
|
// MinIn .
|
|
func (cmb *Combo) MinIn(min int) *Combo {
|
|
cmb.Min.In = min
|
|
return cmb
|
|
}
|
|
|
|
// MinRange .
|
|
func (cmb *Combo) MinRange(min int) *Combo {
|
|
cmb.Min.Range = min
|
|
return cmb
|
|
}
|
|
|
|
// MinNotEQ .
|
|
func (cmb *Combo) MinNotEQ(min int) *Combo {
|
|
cmb.Min.NotEQ = min
|
|
return cmb
|
|
}
|
|
|
|
// MinNotIn .
|
|
func (cmb *Combo) MinNotIn(min int) *Combo {
|
|
cmb.Min.NotIn = min
|
|
return cmb
|
|
}
|
|
|
|
// MinNotRange .
|
|
func (cmb *Combo) MinNotRange(min int) *Combo {
|
|
cmb.Min.NotRange = min
|
|
return cmb
|
|
}
|
|
|
|
// MinAll .
|
|
func (cmb *Combo) MinAll(min int) *Combo {
|
|
cmb.Min.Min = min
|
|
return cmb
|
|
}
|
|
|
|
type groupBy struct {
|
|
Mode enhancedType `json:"mode"`
|
|
Field string `json:"field"`
|
|
Order []map[string]string `json:"order"`
|
|
}
|
|
|
|
type enhance struct {
|
|
Mode enhancedType `json:"mode"`
|
|
Field string `json:"field"`
|
|
Order []map[string]string `json:"order,omitempty"`
|
|
Size int `json:"size,omitempty"`
|
|
}
|
|
|
|
// Elastic clastic instance
|
|
type Elastic struct {
|
|
c *Config
|
|
client *httpx.Client
|
|
}
|
|
|
|
// NewElastic .
|
|
func NewElastic(c *Config) *Elastic {
|
|
if c == nil {
|
|
c = &Config{
|
|
Host: _defaultHost,
|
|
HTTPClient: _defaultHTTPClient,
|
|
}
|
|
}
|
|
return &Elastic{
|
|
c: c,
|
|
client: httpx.NewClient(c.HTTPClient),
|
|
}
|
|
}
|
|
|
|
// Request request to elastic
|
|
type Request struct {
|
|
*Elastic
|
|
q query
|
|
business string
|
|
}
|
|
|
|
// NewRequest new a request every search query
|
|
func (e *Elastic) NewRequest(business string) *Request {
|
|
return &Request{
|
|
Elastic: e,
|
|
business: business,
|
|
q: query{
|
|
Fields: []string{},
|
|
Highlight: false,
|
|
OrderScoreFirst: true,
|
|
OrderRandomSeed: "",
|
|
Pn: 1,
|
|
Ps: 10,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Fields add query fields
|
|
func (r *Request) Fields(fields ...string) *Request {
|
|
r.q.Fields = append(r.q.Fields, fields...)
|
|
return r
|
|
}
|
|
|
|
// Index add query index
|
|
func (r *Request) Index(indexes ...string) *Request {
|
|
r.q.From = strings.Join(indexes, ",")
|
|
return r
|
|
}
|
|
|
|
// IndexByMod index by mod
|
|
func (r *Request) IndexByMod(prefix string, val, mod int64) *Request {
|
|
tmp := mod - 1
|
|
var digit int
|
|
for tmp > 0 {
|
|
tmp /= 10
|
|
digit++
|
|
}
|
|
format := fmt.Sprintf("%s_%%0%dd", prefix, digit)
|
|
r.q.From = fmt.Sprintf(format, val%mod)
|
|
return r
|
|
}
|
|
|
|
// IndexByTime index by time
|
|
func (r *Request) IndexByTime(prefix string, typ indexType, begin, end time.Time) *Request {
|
|
var (
|
|
buf bytes.Buffer
|
|
index string
|
|
indexes = make(map[string]struct{})
|
|
)
|
|
for {
|
|
year := begin.Format("2006")
|
|
month := begin.Format("01")
|
|
switch typ {
|
|
case IndexTypeYear:
|
|
index = strings.Join([]string{prefix, year}, "_")
|
|
case IndexTypeMonth:
|
|
index = strings.Join([]string{prefix, year, month}, "_")
|
|
case IndexTypeDay:
|
|
day := begin.Format("02")
|
|
index = strings.Join([]string{prefix, year, month, day}, "_")
|
|
case IndexTypeWeek:
|
|
index = strings.Join([]string{prefix, year, month, weeks[begin.Day()/8]}, "_")
|
|
}
|
|
if begin.After(end) && begin.Day() != end.Day() {
|
|
break
|
|
}
|
|
indexes[index] = struct{}{}
|
|
begin = begin.AddDate(0, 0, 1)
|
|
}
|
|
for i := range indexes {
|
|
buf.WriteString(i)
|
|
buf.WriteString(",")
|
|
}
|
|
r.q.From = strings.TrimSuffix(buf.String(), ",")
|
|
return r
|
|
}
|
|
|
|
// OrderScoreFirst switch for order score first
|
|
func (r *Request) OrderScoreFirst(v bool) *Request {
|
|
r.q.OrderScoreFirst = v
|
|
return r
|
|
}
|
|
|
|
// OrderRandomSeed switch for order random
|
|
func (r *Request) OrderRandomSeed(v string) *Request {
|
|
r.q.OrderRandomSeed = v
|
|
return r
|
|
}
|
|
|
|
// Highlight switch from highlight
|
|
func (r *Request) Highlight(v bool) *Request {
|
|
r.q.Highlight = v
|
|
return r
|
|
}
|
|
|
|
// Pn page number
|
|
func (r *Request) Pn(v int) *Request {
|
|
r.q.Pn = v
|
|
return r
|
|
}
|
|
|
|
// Ps page size
|
|
func (r *Request) Ps(v int) *Request {
|
|
r.q.Ps = v
|
|
return r
|
|
}
|
|
|
|
// Order filed sort
|
|
func (r *Request) Order(field, sort string) *Request {
|
|
if sort != OrderAsc {
|
|
sort = OrderDesc
|
|
}
|
|
r.q.Order = append(r.q.Order, map[string]string{field: sort})
|
|
return r
|
|
}
|
|
|
|
// WhereEq where qual
|
|
func (r *Request) WhereEq(field string, eq interface{}) *Request {
|
|
if r.q.Where.Eq == nil {
|
|
r.q.Where.Eq = make(map[string]interface{})
|
|
}
|
|
r.q.Where.Eq[field] = eq
|
|
return r
|
|
}
|
|
|
|
// WhereOr where or
|
|
func (r *Request) WhereOr(field string, or interface{}) *Request {
|
|
if r.q.Where.Or == nil {
|
|
r.q.Where.Or = make(map[string]interface{})
|
|
}
|
|
r.q.Where.Or[field] = or
|
|
return r
|
|
}
|
|
|
|
// WhereIn where in
|
|
func (r *Request) WhereIn(field string, in interface{}) *Request {
|
|
if r.q.Where.In == nil {
|
|
r.q.Where.In = make(map[string][]interface{})
|
|
}
|
|
switch v := in.(type) {
|
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, string:
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], v)
|
|
case []int:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []int64:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []string:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []int8:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []int16:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []int32:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []uint:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []uint8:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []uint16:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []uint32:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
case []uint64:
|
|
for _, i := range v {
|
|
r.q.Where.In[field] = append(r.q.Where.In[field], i)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// WhereRange where range
|
|
func (r *Request) WhereRange(field string, start, end interface{}, scope rangeScope) *Request {
|
|
if r.q.Where.Range == nil {
|
|
r.q.Where.Range = make(map[string]string)
|
|
}
|
|
if start == nil {
|
|
start = ""
|
|
}
|
|
if end == nil {
|
|
end = ""
|
|
}
|
|
switch scope {
|
|
case RangeScopeLoRo:
|
|
r.q.Where.Range[field] = fmt.Sprintf("(%v,%v)", start, end)
|
|
case RangeScopeLoRc:
|
|
r.q.Where.Range[field] = fmt.Sprintf("(%v,%v]", start, end)
|
|
case RangeScopeLcRo:
|
|
r.q.Where.Range[field] = fmt.Sprintf("[%v,%v)", start, end)
|
|
case RangeScopeLcRc:
|
|
r.q.Where.Range[field] = fmt.Sprintf("[%v,%v]", start, end)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// WhereNot where not
|
|
func (r *Request) WhereNot(typ notType, fields ...string) *Request {
|
|
if r.q.Where.Not == nil {
|
|
r.q.Where.Not = make(map[notType]map[string]bool)
|
|
}
|
|
if r.q.Where.Not[typ] == nil {
|
|
r.q.Where.Not[typ] = make(map[string]bool)
|
|
}
|
|
for _, v := range fields {
|
|
r.q.Where.Not[typ][v] = true
|
|
}
|
|
return r
|
|
}
|
|
|
|
// WhereLike where like
|
|
func (r *Request) WhereLike(fields, words []string, or bool, level likeLevel) *Request {
|
|
if len(fields) == 0 || len(words) == 0 {
|
|
return r
|
|
}
|
|
l := whereLike{Fields: fields, Words: words, Or: or, Level: level}
|
|
r.q.Where.Like = append(r.q.Where.Like, l)
|
|
return r
|
|
}
|
|
|
|
// WhereCombo where combo
|
|
func (r *Request) WhereCombo(cmb ...*Combo) *Request {
|
|
r.q.Where.Combo = append(r.q.Where.Combo, cmb...)
|
|
return r
|
|
}
|
|
|
|
// GroupBy where group by
|
|
func (r *Request) GroupBy(mode enhancedType, field string, order []map[string]string) *Request {
|
|
for _, i := range order {
|
|
for k, v := range i {
|
|
if v != OrderAsc {
|
|
i[k] = OrderDesc
|
|
}
|
|
}
|
|
}
|
|
r.q.Where.Enhanced = append(r.q.Where.Enhanced, groupBy{Mode: mode, Field: field, Order: order})
|
|
return r
|
|
}
|
|
|
|
// Sum where enhance sum
|
|
func (r *Request) Sum(field string) *Request {
|
|
r.q.Where.Enhanced = append(r.q.Where.Enhanced, enhance{Mode: EnhancedModeSum, Field: field})
|
|
return r
|
|
}
|
|
|
|
// Scan parse the query response data
|
|
func (r *Request) Scan(ctx context.Context, result interface{}) (err error) {
|
|
q, err := r.q.string()
|
|
if err != nil {
|
|
return
|
|
}
|
|
params := url.Values{}
|
|
params.Add("business", r.business)
|
|
params.Add("query", q)
|
|
response := new(response)
|
|
if err = r.client.Get(ctx, r.c.Host+_pathQuery, "", params, &response); err != nil {
|
|
return
|
|
}
|
|
if !ecode.Int(response.Code).Equal(ecode.OK) {
|
|
return ecode.Int(response.Code)
|
|
}
|
|
err = errors.Wrapf(json.Unmarshal(response.Data, &result), "scan(%s)", response.Data)
|
|
return
|
|
}
|
|
|
|
// Params get query parameters
|
|
func (r *Request) Params() string {
|
|
q, _ := r.q.string()
|
|
return fmt.Sprintf("business=%s&query=%s", r.business, q)
|
|
}
|