feat: elasticsearch, influxdb.

This commit is contained in:
Bobo
2025-06-25 19:50:51 +08:00
parent 735ca567bf
commit d31ab9cdf3
16 changed files with 3217 additions and 64 deletions

View File

@@ -0,0 +1,55 @@
# ElasticSearch
## 概念对比
| ES存储结构 | RDBMS存储结构 |
|----------|-----------|
| Index | 表 |
| Document | 行 |
| Field | 表字段 |
| Mapping | 表结构定义 |
## mapping
- 动态映射dynamic mapping
- 显式映射explicit mapping
- 严格映射strict mappings
## Docker部署
### 拉取镜像
```bash
docker pull bitnami/elasticsearch:latest
```
### 启动容器
```bash
docker run -itd \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e ELASTICSEARCH_USERNAME=elastic \
-e ELASTICSEARCH_PASSWORD=elastic \
-e xpack.security.enabled=true \
-e discovery.type=single-node \
-e http.cors.enabled=true \
-e http.cors.allow-origin=http://localhost:13580,http://127.0.0.1:13580 \
-e http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization \
-e http.cors.allow-credentials=true \
bitnami/elasticsearch:latest
```
安装管理工具:
```bash
docker pull appbaseio/dejavu:latest
docker run -itd \
--name dejavu-test \
-p 13580:1358 \
appbaseio/dejavu:latest
```
访问管理工具:<http://localhost:13580/>

View File

@@ -0,0 +1,446 @@
package elasticsearch
import (
"bytes"
"context"
"encoding/json"
"io"
"github.com/go-kratos/kratos/v2/encoding"
_ "github.com/go-kratos/kratos/v2/encoding/json"
"github.com/go-kratos/kratos/v2/log"
elasticsearchV9 "github.com/elastic/go-elasticsearch/v9"
esapiV9 "github.com/elastic/go-elasticsearch/v9/esapi"
conf "github.com/tx7do/kratos-bootstrap/api/gen/go/conf/v1"
pagination "github.com/tx7do/kratos-bootstrap/api/gen/go/pagination/v1"
)
type Client struct {
cli *elasticsearchV9.Client
log *log.Helper
codec encoding.Codec
}
func NewClient(logger log.Logger, cfg *conf.Bootstrap) (*Client, error) {
c := &Client{
log: log.NewHelper(log.With(logger, "module", "elasticsearch-client")),
codec: encoding.GetCodec("json"),
}
if err := c.createESClient(cfg); err != nil {
return nil, err
}
return c, nil
}
// createESClient 创建Elasticsearch客户端
func (c *Client) createESClient(cfg *conf.Bootstrap) error {
if cfg.Data == nil || cfg.Data.ElasticSearch == nil {
return nil // No Elasticsearch configuration provided
}
cli, err := elasticsearchV9.NewClient(
elasticsearchV9.Config{
Addresses: cfg.Data.ElasticSearch.GetAddresses(),
Username: cfg.Data.ElasticSearch.GetUsername(),
Password: cfg.Data.ElasticSearch.GetPassword(),
},
)
if err != nil {
c.log.Errorf("failed to create elasticsearch client: %v", err)
return err
}
c.cli = cli
return nil
}
func (c *Client) Close() {
}
// CheckConnectStatus 检查Elasticsearch连接
func (c *Client) CheckConnectStatus() bool {
if c.cli == nil {
return false
}
resp, err := c.cli.Info()
if err != nil {
c.log.Errorf("failed to connect to elasticsearch: %v", err)
return false
}
defer func(Body io.ReadCloser) {
if err = Body.Close(); err != nil {
c.log.Errorf("failed to close response body: %v", err)
}
}(resp.Body)
if resp.IsError() {
c.log.Errorf("Error: %s", resp.String())
return false
}
var r map[string]interface{}
if err = json.NewDecoder(resp.Body).Decode(&r); err != nil {
log.Fatalf("Error parsing the response body: %s", err)
return false
}
c.log.Infof("Client Version: %s", elasticsearchV9.Version)
c.log.Infof("Server Version: %s", r["version"].(map[string]interface{})["number"])
return true
}
// IndexExists 检查索引是否存在
func (c *Client) IndexExists(ctx context.Context, indexName string) (bool, error) {
resp, err := c.cli.Indices.Exists(
[]string{indexName},
c.cli.Indices.Exists.WithContext(ctx),
)
if err != nil {
c.log.Errorf("failed to check if index exists: %v", err)
return false, err
}
return !resp.IsError(), nil
}
// CreateIndex 创建一条索引
//
// 如果mapping为空("")则表示不创建模型
func (c *Client) CreateIndex(ctx context.Context, indexName string, mapping, settings string) error {
exist, err := c.IndexExists(ctx, indexName)
if err != nil {
return err
}
if exist {
return ErrIndexAlreadyExists
}
body, err := MergeOptions(mapping, settings)
if err != nil {
c.log.Errorf("failed to merge options: %v", err)
return err
}
resp, err := c.cli.Indices.Create(
indexName,
c.cli.Indices.Create.WithContext(ctx),
c.cli.Indices.Create.WithBody(bytes.NewReader([]byte(body))),
)
if err != nil {
c.log.Errorf("failed to create index: %v", err)
return err
}
if resp.IsError() {
var errResp *ErrorResponse
if errResp, err = ParseErrorMessage(resp.Body); err != nil {
c.log.Errorf("failed to parse error message: %v", err)
return err
}
c.log.Errorf("create index failed: %s", errResp.Error)
return ErrCreateIndex
}
return nil
}
// DeleteIndex 删除一条索引
func (c *Client) DeleteIndex(ctx context.Context, indexName string) error {
exist, err := c.IndexExists(ctx, indexName)
if err != nil {
c.log.Errorf("failed to check if index exists: %v", err)
return err
}
if !exist {
return ErrIndexNotFound
}
resp, err := c.cli.Indices.Delete(
[]string{indexName},
c.cli.Indices.Delete.WithContext(ctx),
)
if err != nil {
c.log.Errorf("failed to delete index: %v", err)
return err
}
if resp.IsError() {
var errResp *ErrorResponse
if errResp, err = ParseErrorMessage(resp.Body); err != nil {
c.log.Errorf("failed to parse error message: %v", err)
return err
}
c.log.Errorf("delete index failed: %s", errResp.Error.Reason)
return ErrDeleteIndex
}
return nil
}
// DeleteDocument 删除一条数据
func (c *Client) DeleteDocument(ctx context.Context, indexName, id string) error {
_, err := c.cli.Delete(
indexName, id,
c.cli.Delete.WithContext(ctx),
)
if err != nil {
c.log.Errorf("failed to delete document: %v", err)
return err
}
return nil
}
// InsertDocument 插入一条数据
func (c *Client) InsertDocument(ctx context.Context, indexName, id string, data interface{}) error {
var err error
var dataBytes []byte
dataBytes, err = json.Marshal(data)
if err != nil {
c.log.Errorf("failed to marshal data: %v", err)
return err
}
var resp *esapiV9.Response
if id == "" {
resp, err = c.cli.Index(
indexName,
bytes.NewReader(dataBytes),
c.cli.Index.WithContext(ctx),
)
} else {
resp, err = c.cli.Create(
indexName, id,
bytes.NewReader(dataBytes),
c.cli.Create.WithContext(ctx),
)
}
if err != nil {
c.log.Errorf("failed to insert document: %v", err)
return err
}
defer func(Body io.ReadCloser) {
if err = Body.Close(); err != nil {
c.log.Errorf("failed to close response body: %v", err)
}
}(resp.Body)
if resp.IsError() {
var errResp *ErrorResponse
if errResp, err = ParseErrorMessage(resp.Body); err != nil {
c.log.Errorf("failed to parse error message: %v", err)
return err
}
c.log.Errorf("insert data failed: %s", errResp.Error.Reason)
return ErrInsertDocument
}
return nil
}
// BatchInsertDocument 批量插入数据
func (c *Client) BatchInsertDocument(ctx context.Context, indexName string, dataSet []interface{}) error {
var buf bytes.Buffer
for _, data := range dataSet {
meta := []byte(`{"index":{}}` + "\n")
dataBytes, err := json.Marshal(data)
if err != nil {
c.log.Errorf("failed to marshal data: %v", err)
continue
}
dataBytes = append(dataBytes, "\n"...)
buf.Grow(len(meta) + len(dataBytes))
buf.Write(meta)
buf.Write(dataBytes)
}
resp, err := c.cli.Bulk(
bytes.NewReader(buf.Bytes()),
c.cli.Bulk.WithContext(ctx),
c.cli.Bulk.WithIndex(indexName),
)
if err != nil {
c.log.Errorf("failed to perform bulk insert: %v", err)
return err
}
defer func(Body io.ReadCloser) {
if err = Body.Close(); err != nil {
c.log.Errorf("failed to close response body: %v", err)
}
}(resp.Body)
if resp.IsError() {
var errResp *ErrorResponse
if errResp, err = ParseErrorMessage(resp.Body); err != nil {
c.log.Errorf("failed to parse error message: %v", err)
return err
}
c.log.Errorf("batch insert data failed: %s", errResp.Error.Reason)
return ErrBatchInsertDocument
}
return nil
}
func (c *Client) UpdateDocument(ctx context.Context, indexName string, pk string, doc interface{}) error {
data, err := json.Marshal(doc)
if err != nil {
c.log.Errorf("failed to marshal data: %v", err)
return err
}
_, err = c.cli.Update(
indexName, pk,
bytes.NewReader(data),
c.cli.Update.WithContext(ctx),
)
if err != nil {
c.log.Errorf("failed to update document: %v", err)
return err
}
return nil
}
// GetDocument 查询数据
func (c *Client) GetDocument(
ctx context.Context,
indexName string,
id string,
sourceFields []string,
out interface{},
) error {
resp, err := c.cli.Get(
indexName, id,
c.cli.Get.WithContext(ctx),
c.cli.Get.WithSource(sourceFields...), // 指定返回的字段
)
if err != nil {
c.log.Errorf("failed to get document: %v", err)
return err
}
if resp.IsError() {
var errResp *ErrorResponse
if errResp, err = ParseErrorMessage(resp.Body); err != nil {
c.log.Errorf("failed to parse error message: %v", err)
return err
}
if resp.StatusCode == 404 {
c.log.Warnf("document not found: %s", errResp.Error.Reason)
return ErrDocumentNotFound
}
c.log.Errorf("get document failed: %s", errResp.Error.Reason)
return ErrGetDocument
}
if err = json.NewDecoder(resp.Body).Decode(&out); err != nil {
c.log.Errorf("failed to decode document: %v", err)
return err
}
return nil
}
func (c *Client) Search(
ctx context.Context,
indexName string,
req *pagination.PagingRequest,
) (*SearchResult, error) {
var query string
ParseQueryString(req.GetQuery())
sortBy := make(map[string]bool)
pageSize := req.GetPageSize()
if pageSize <= 0 {
pageSize = 20 // Default page size
}
return c.search(ctx, indexName, query, nil, sortBy, int(req.GetPage()), int(pageSize))
}
// search 查询数据
//
// @param ctx 上下文
// @param indexName 索引名
// @param query 查询条件例如field1:value1 AND field2:value2
// @param sourceFields 指定返回的字段传入nil表示返回所有字段
// @param sortBy 排序
// @param from 分页的页码
// @param pageSize 分页每页的行数
func (c *Client) search(
ctx context.Context,
indexName string,
query string,
sourceFields []string,
sortBy map[string]bool,
from, pageSize int,
) (*SearchResult, error) {
var sorts []string
for k, v := range sortBy {
if v {
sorts = append(sorts, k+":asc")
} else {
sorts = append(sorts, k+":desc")
}
}
resp, err := c.cli.Search(
c.cli.Search.WithContext(ctx),
c.cli.Search.WithIndex(indexName),
c.cli.Search.WithFrom(from),
c.cli.Search.WithSize(pageSize),
c.cli.Search.WithSort(sorts...),
c.cli.Search.WithQuery(query),
c.cli.Search.WithSource(sourceFields...), // 指定返回的字段
)
if err != nil {
c.log.Errorf("failed to search documents: %v", err)
return nil, err
}
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
c.log.Errorf("failed to close response body: %v", err)
}
}(resp.Body)
if resp.IsError() {
var errResp *ErrorResponse
if errResp, err = ParseErrorMessage(resp.Body); err != nil {
return nil, err
}
c.log.Errorf("search document failed: %s", errResp.Error.Reason)
return nil, ErrSearchDocument
}
var searchResult SearchResult
if err = json.NewDecoder(resp.Body).Decode(&searchResult); err != nil {
c.log.Errorf("failed to decode search result: %v", err)
return nil, err
}
return &searchResult, nil
}

View File

@@ -0,0 +1,359 @@
package elasticsearch
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"strconv"
"strings"
"testing"
"time"
"github.com/go-kratos/kratos/v2/log"
"github.com/stretchr/testify/assert"
conf "github.com/tx7do/kratos-bootstrap/api/gen/go/conf/v1"
)
const (
userIndex = "user"
tweetIndex = "tweet"
sensorIndex = "sensor"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Phone string `json:"phone"`
Birth time.Time `json:"birth"`
Height float32 `json:"height"`
Smoke bool `json:"smoke"`
Home string `json:"home"`
}
// UserMapping 定义用户mapping
const UserMapping = `
{
"mappings": {
"properties": {
"name": {"type": "text"},
"age": {"type": "byte"},
"phone": {"type": "text"},
"birth": {"type": "date"},
"height": {"type": "float"},
"smoke": {"type": "boolean"},
"home": {"type": "geo_point"}
}
}
}`
type Tweet struct {
User string `json:"user"` // 用户
Message string `json:"message"` // 微博内容
Retweets int `json:"retweets"` // 转发数
Image string `json:"image,omitempty"` // 图片
Created time.Time `json:"created,omitempty"` // 创建时间
Tags []string `json:"tags,omitempty"` // 标签
Location string `json:"location,omitempty"` //位置
//Suggest *elasticsearchV9.SuggestField `json:"suggest_field,omitempty"`
}
const TweetMapping = `
{
"mappings": {
"properties": {
"user": {"type": "keyword"},
"message": {"type": "text"},
"image": {"type": "keyword"},
"created": {"type": "date"},
"tags": {"type": "keyword"},
"location": {"type": "geo_point"},
"suggest_field": {"type": "completion"}
}
}
}`
type Sensor struct {
Id int `json:"id" bson:"_id,omitempty"`
Type string `json:"type" bson:"type,omitempty"`
Location string `json:"location,omitempty" bson:"location,omitempty"`
}
type SensorData struct {
Id string `json:"id" bson:"_id,omitempty"`
Time time.Time `json:"time" bson:"created,omitempty"`
SensorId int `json:"sensor_id" bson:"sensor_id,omitempty"`
Temperature float64 `json:"temperature" bson:"temperature,omitempty"`
CPU float64 `json:"cpu" bson:"cpu,omitempty"`
}
const SensorMapping = `
{
"mappings": {
"properties": {
"sensor_id": {"type": "integer"},
"temperature": {"type": "double"},
"cpu": {"type": "double"},
"location": {"type": "geo_point"}
}
}
}`
func createTestClient() *Client {
cli, _ := NewClient(
log.DefaultLogger,
&conf.Bootstrap{
Data: &conf.Data{
ElasticSearch: &conf.Data_ElasticSearch{
Addresses: []string{"http://localhost:9200"},
Username: "elastic",
Password: "elastic",
},
},
},
)
return cli
}
func TestNewClient(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
client.CheckConnectStatus()
}
func TestCreateIndex(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
var esCtx = context.Background()
{
_ = client.DeleteIndex(esCtx, userIndex)
err := client.CreateIndex(esCtx, userIndex, UserMapping, "")
assert.Nil(t, err)
}
{
_ = client.DeleteIndex(esCtx, tweetIndex)
err := client.CreateIndex(esCtx, tweetIndex, TweetMapping, "")
assert.Nil(t, err)
}
{
_ = client.DeleteIndex(esCtx, sensorIndex)
err := client.CreateIndex(esCtx, sensorIndex, SensorMapping, "")
assert.Nil(t, err)
}
}
func TestDeleteIndex(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
var esCtx = context.Background()
err := client.DeleteIndex(esCtx, userIndex)
assert.Nil(t, err)
err = client.DeleteIndex(esCtx, tweetIndex)
assert.Nil(t, err)
err = client.DeleteIndex(esCtx, sensorIndex)
assert.Nil(t, err)
}
func TestInsertDocument(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
var esCtx = context.Background()
{
// http://localhost:9200/user/_search?q=*&pretty
loc, _ := time.LoadLocation("Local")
birth, _ := time.ParseInLocation("2006-01-02", "1991-04-25", loc)
userOne := User{
Name: "张三",
Age: 23,
Phone: "17600000000",
Birth: birth,
Height: 170.5,
Home: "41.40338,2.17403",
}
err := client.InsertDocument(esCtx, userIndex, "", userOne)
assert.Nil(t, err)
}
{
tweetOne := Tweet{User: "olive", Message: "打酱油的一天", Retweets: 0}
err := client.InsertDocument(esCtx, tweetIndex, "", tweetOne)
assert.Nil(t, err)
}
}
func TestBatchInsertDocument(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
var esCtx = context.Background()
{
loc, _ := time.LoadLocation("Local")
// 生日
birthSlice := []string{"1991-04-25", "1990-01-15", "1989-11-05", "1988-01-25", "1994-10-12"}
// 姓名
nameSlice := []string{"李四", "张飞", "赵云", "关羽", "刘备"}
var users []interface{}
for i := 1; i < 20; i++ {
birth, _ := time.ParseInLocation("2006-01-02", birthSlice[rand.Intn(len(birthSlice))], loc)
height, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", rand.Float32()+175.0), 32)
user := User{
Name: nameSlice[rand.Intn(len(nameSlice))],
Age: rand.Intn(10) + 18,
Phone: "1760000000" + strconv.Itoa(i),
Birth: birth,
Height: float32(height),
Home: "41.40338,2.17403",
}
users = append(users, user)
}
err := client.BatchInsertDocument(esCtx, userIndex, users)
assert.Nil(t, err)
}
}
func TestGetDocument(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
var esCtx = context.Background()
var user User
const id = "N_1fm5cBE8GqVkmNBLNY"
err := client.GetDocument(esCtx, userIndex, id, nil, &user)
assert.Equal(t, err, ErrDocumentNotFound)
assert.NotNil(t, user)
}
func TestSearch(t *testing.T) {
client := createTestClient()
assert.NotNil(t, client)
var esCtx = context.Background()
//// 创建索引并插入测试数据
//_ = client.DeleteIndex(esCtx, userIndex)
//err := client.CreateIndex(esCtx, userIndex, UserMapping, "")
//assert.Nil(t, err)
//
//userOne := User{
// Name: "张三",
// Age: 23,
// Phone: "17600000000",
// Height: 170.5,
// Home: "41.40338,2.17403",
//}
//err = client.InsertDocument(esCtx, userIndex, "", userOne)
//assert.Nil(t, err)
// 测试Search方法
query := "name:张三"
sortBy := map[string]bool{"age": true}
from := 0
pageSize := 10
searchResult, err := client.search(
esCtx, userIndex, query, nil, sortBy, from, pageSize,
)
assert.Nil(t, err)
assert.NotNil(t, searchResult)
var users []User
for _, hit := range searchResult.Hits.Hits {
var user User
if err = json.Unmarshal(hit.Source, &user); err != nil {
t.Errorf("Failed to unmarshal hit: %v", err)
continue
}
users = append(users, user)
}
t.Logf("Search result: %v", users)
}
func TestMergeOptions(t *testing.T) {
mapping := `{
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "integer"
}
}
}`
settings := `{
"index": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}`
//expected := `{"mappings":{"properties":{"name":{"type":"text"},"age":{"type":"integer"}}},"settings":{"index":{"number_of_shards":1,"number_of_replicas":0}}}`
result, err := MergeOptions(mapping, settings)
assert.Nil(t, err)
//assert.Equal(t, expected, result)
t.Log(result)
}
func TestParseQueryString(t *testing.T) {
// 测试单个键值对的查询字符串
query := `{"name":"张三"}`
result := ParseQueryString(query)
assert.NotNil(t, result)
assert.Equal(t, []string{"name:张三"}, result)
// 测试多个键值对的查询字符串
query = `[{"name":"张三"},{"age":"23"}]`
result = ParseQueryString(query)
assert.NotNil(t, result)
assert.Equal(t, []string{"name:张三", "age:23"}, result)
t.Log(strings.Join(result, " AND "))
// 测试无效的查询字符串
query = `invalid`
result = ParseQueryString(query)
assert.Nil(t, result)
}
func TestMakeQueryString(t *testing.T) {
// 测试 AND 查询
andQuery := `{"name":"张三","age":"23"}`
orQuery := ``
result := MakeQueryString(andQuery, orQuery)
assert.Equal(t, "name:张三 AND age:23", result)
// 测试 OR 查询
andQuery = ``
orQuery = `[{"city":"北京"},{"country":"中国"}]`
result = MakeQueryString(andQuery, orQuery)
assert.Equal(t, "city:北京 OR country:中国", result)
// 测试 AND 和 OR 查询同时存在
andQuery = `{"name":"张三"}`
orQuery = `[{"city":"北京"},{"country":"中国"}]`
result = MakeQueryString(andQuery, orQuery)
assert.Equal(t, "name:张三 AND (city:北京 OR country:中国)", result)
// 测试空查询
andQuery = ``
orQuery = ``
result = MakeQueryString(andQuery, orQuery)
assert.Equal(t, "", result)
}

View File

@@ -0,0 +1,38 @@
package elasticsearch
import "github.com/go-kratos/kratos/v2/errors"
var (
// ErrRequestFailed is returned when a request to Elasticsearch fails.
ErrRequestFailed = errors.InternalServer("REQUEST_FAILED", "request failed")
// ErrIndexNotFound is returned when the specified index does not exist.
ErrIndexNotFound = errors.InternalServer("INDEX_NOT_FOUND", "index not found")
// ErrIndexAlreadyExists is returned when trying to create an index that already exists.
ErrIndexAlreadyExists = errors.InternalServer("INDEX_ALREADY_EXISTS", "index already exists")
ErrCreateIndex = errors.InternalServer("CREATE_INDEX_FAILED", "failed to create index")
ErrDeleteIndex = errors.InternalServer("DELETE_INDEX_FAILED", "failed to delete index")
// ErrDocumentNotFound is returned when a document is not found in the index.
ErrDocumentNotFound = errors.InternalServer("DOCUMENT_NOT_FOUND", "document not found")
// ErrDocumentAlreadyExists is returned when trying to create a document that already exists.
ErrDocumentAlreadyExists = errors.InternalServer("DOCUMENT_ALREADY_EXISTS", "document already exists")
// ErrInvalidQuery is returned when the query provided to Elasticsearch is invalid.
ErrInvalidQuery = errors.InternalServer("INVALID_QUERY", "invalid query")
// ErrUnmarshalResponse is returned when the response from Elasticsearch cannot be unmarshalled.
ErrUnmarshalResponse = errors.InternalServer("UNMARSHAL_RESPONSE_FAILED", "failed to unmarshal response")
ErrInsertDocument = errors.InternalServer("INSERT_DOCUMENT_FAILED", "failed to insert document")
ErrBatchInsertDocument = errors.InternalServer("BATCH_INSERT_DOCUMENT_FAILED", "failed to batch insert documents")
ErrGetDocument = errors.InternalServer("GET_DOCUMENT_FAILED", "failed to get document")
ErrSearchDocument = errors.InternalServer("SEARCH_DOCUMENT_FAILED", "failed to search document")
)

View File

@@ -0,0 +1,35 @@
module github.com/tx7do/kratos-bootstrap/database/elasticsearch
go 1.23.0
toolchain go1.23.3
replace github.com/tx7do/kratos-bootstrap/api => ../../api
require (
github.com/elastic/go-elasticsearch/v9 v9.0.0
github.com/go-kratos/kratos/v2 v2.8.4
github.com/stretchr/testify v1.10.0
github.com/tx7do/kratos-bootstrap/api v0.0.25
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/elastic-transport-go/v8 v8.7.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/gnostic v0.7.0 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
package elasticsearch
import "encoding/json"
// ErrorResponse 表示 Elasticsearch 错误响应的结构
type ErrorResponse struct {
Error struct {
RootCause []struct {
Type string `json:"type"`
Reason string `json:"reason"`
} `json:"root_cause"`
Type string `json:"type"`
Reason string `json:"reason"`
CausedBy struct {
Type string `json:"type"`
Reason string `json:"reason"`
} `json:"caused_by,omitempty"`
} `json:"error"`
Status int `json:"status"`
}
type SearchResult struct {
Took int `json:"took"`
TimedOut bool `json:"timed_out"`
Hits struct {
Total struct {
Value int `json:"value"`
Relation string `json:"relation"`
} `json:"total"`
Hits []struct {
Index string `json:"_index"`
Type string `json:"_type"`
ID string `json:"_id"`
Score float64 `json:"_score"`
Source json.RawMessage `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}

View File

@@ -0,0 +1,111 @@
package elasticsearch
import (
"encoding/json"
"io"
"strings"
"github.com/go-kratos/kratos/v2/encoding"
_ "github.com/go-kratos/kratos/v2/encoding/json"
"github.com/go-kratos/kratos/v2/log"
)
// ParseErrorMessage 解析 Elasticsearch 错误消息
func ParseErrorMessage(body io.ReadCloser) (*ErrorResponse, error) {
defer body.Close()
var errorResponse ErrorResponse
if err := json.NewDecoder(body).Decode(&errorResponse); err != nil {
return nil, ErrUnmarshalResponse
}
return &errorResponse, nil
}
// MergeOptions 合并 Elasticsearch 索引的映射和设置
func MergeOptions(mapping, settings string) (string, error) {
codec := encoding.GetCodec("json")
body := make(map[string]interface{})
if mapping != "" {
var mappingObj map[string]interface{}
if err := codec.Unmarshal([]byte(mapping), &mappingObj); err != nil {
log.Errorf("failed to unmarshal mapping: %v", err)
return "", err
}
if existingMappings, ok := mappingObj["mappings"]; ok {
body["mappings"] = existingMappings
} else {
body["mappings"] = mappingObj
}
}
if settings != "" {
var settingsObj map[string]interface{}
if err := codec.Unmarshal([]byte(settings), &settingsObj); err != nil {
log.Errorf("failed to unmarshal settings: %v", err)
return "", err
}
// 检查 settings 是否包含 settings 字段
if existingSettings, ok := settingsObj["settings"]; ok {
body["settings"] = existingSettings
} else {
body["settings"] = settingsObj
}
}
bodyBytes, err := codec.Marshal(body)
if err != nil {
log.Errorf("failed to marshal request body: %v", err)
return "", err
}
return string(bodyBytes), nil
}
func ParseQueryString(query string) []string {
codec := encoding.GetCodec("json")
var err error
queryMap := make(map[string]string)
if err = codec.Unmarshal([]byte(query), &queryMap); err == nil {
var queries []string
for k, v := range queryMap {
queries = append(queries, k+":"+v)
}
return queries
}
var queryMapArray []map[string]string
if err = codec.Unmarshal([]byte(query), &queryMapArray); err == nil {
var queries []string
for _, item := range queryMapArray {
for k, v := range item {
queries = append(queries, k+":"+v)
}
}
return queries
}
return nil
}
func MakeQueryString(andQuery, orQuery string) string {
a := ParseQueryString(andQuery)
o := ParseQueryString(orQuery)
if len(a) == 0 && len(o) == 0 {
return ""
}
if len(a) > 0 && len(o) == 0 {
return strings.Join(a, " AND ")
} else if len(a) == 0 && len(o) > 0 {
return strings.Join(o, " OR ")
} else if len(a) > 0 && len(o) > 0 {
return strings.Join(a, " AND ") + " AND (" + strings.Join(o, " OR ") + ")"
} else {
return strings.Join(a, " AND ") + " AND " + strings.Join(o, " OR ")
}
}