378 lines
9.4 KiB
Go
378 lines
9.4 KiB
Go
/*
|
|
* Copyright 2025 coze-dev Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
|
|
"github.com/bytedance/sonic"
|
|
|
|
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/search"
|
|
searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity"
|
|
"github.com/coze-dev/coze-studio/backend/infra/contract/es"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
|
|
"github.com/coze-dev/coze-studio/backend/pkg/logs"
|
|
)
|
|
|
|
var searchInstance *searchImpl
|
|
|
|
func NewDomainService(ctx context.Context, e es.Client) Search {
|
|
return &searchImpl{
|
|
esClient: e,
|
|
}
|
|
}
|
|
|
|
type searchImpl struct {
|
|
esClient es.Client
|
|
}
|
|
|
|
type fieldName string
|
|
|
|
const (
|
|
fieldOfSpaceID = "space_id"
|
|
fieldOfOwnerID = "owner_id"
|
|
fieldOfID = "id"
|
|
fieldOfAPPID = "app_id"
|
|
fieldOfName = "name"
|
|
fieldOfNameRaw = "name.raw"
|
|
fieldOfHasPublished = "has_published"
|
|
fieldOfStatus = "status"
|
|
fieldOfType = "type"
|
|
fieldOfIsFav = "is_fav"
|
|
fieldOfIsRecentlyOpen = "is_recently_open"
|
|
|
|
resTypeSearchAll = -1
|
|
)
|
|
|
|
func (s *searchImpl) SearchProjects(ctx context.Context, req *searchEntity.SearchProjectsRequest) (resp *searchEntity.SearchProjectsResponse, err error) {
|
|
logs.CtxDebugf(ctx, "[SearchProjects] search : %s", conv.DebugJsonToStr(req))
|
|
searchReq := &es.Request{
|
|
Query: &es.Query{
|
|
Bool: &es.BoolQuery{},
|
|
},
|
|
}
|
|
|
|
if req.ProjectID != 0 { // precise search
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfID, conv.Int64ToStr(req.ProjectID)))
|
|
} else {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfSpaceID, conv.Int64ToStr(req.SpaceID)))
|
|
}
|
|
|
|
if req.Name != "" {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewContainsQuery(fieldOfNameRaw, req.Name))
|
|
}
|
|
|
|
if req.IsPublished {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfHasPublished, conv.BoolToInt(req.IsPublished)))
|
|
}
|
|
|
|
if req.IsFav {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfIsFav, conv.BoolToInt(req.IsFav)))
|
|
}
|
|
|
|
if req.IsRecentlyOpen {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfIsRecentlyOpen, conv.BoolToInt(req.IsRecentlyOpen)))
|
|
}
|
|
|
|
if req.OwnerID > 0 {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfOwnerID, conv.Int64ToStr(req.OwnerID)))
|
|
}
|
|
|
|
if len(req.Status) > 0 {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewInQuery(fieldOfStatus, req.Status))
|
|
}
|
|
|
|
if len(req.Types) > 0 {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewInQuery(fieldOfType, req.Types))
|
|
}
|
|
|
|
reqLimit := 100
|
|
if req.Limit > 0 {
|
|
reqLimit = int(req.Limit)
|
|
}
|
|
|
|
realLimit := reqLimit + 1
|
|
searchReq.Size = &realLimit
|
|
|
|
if req.OrderFiledName == "" {
|
|
req.OrderFiledName = model.FieldOfUpdateTime
|
|
}
|
|
|
|
searchReq.Sort = []es.SortFiled{
|
|
{
|
|
Field: req.OrderFiledName,
|
|
Asc: req.OrderAsc,
|
|
},
|
|
}
|
|
|
|
if req.Cursor != "" && req.Cursor != "0" {
|
|
searchReq.SearchAfter = []any{
|
|
fieldValueCaster(req.OrderFiledName, req.Cursor),
|
|
}
|
|
}
|
|
|
|
result, err := s.esClient.Search(ctx, projectIndexName, searchReq)
|
|
if err != nil {
|
|
logs.CtxDebugf(ctx, "[Serarch.DO] err : %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
hits := result.Hits.Hits
|
|
|
|
hasMore := func() bool {
|
|
if len(hits) > reqLimit {
|
|
return true
|
|
}
|
|
return false
|
|
}()
|
|
|
|
if hasMore {
|
|
hits = hits[:reqLimit]
|
|
}
|
|
|
|
docs := make([]*searchEntity.ProjectDocument, 0, len(hits))
|
|
for _, hit := range hits {
|
|
doc, err := hit2AppDocument(hit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
docs = append(docs, doc)
|
|
}
|
|
|
|
nextCursor := ""
|
|
if len(docs) > 0 {
|
|
nextCursor = formatProjectNextCursor(req.OrderFiledName, docs[len(docs)-1])
|
|
}
|
|
if nextCursor == "" {
|
|
hasMore = false
|
|
}
|
|
|
|
resp = &searchEntity.SearchProjectsResponse{
|
|
Data: docs,
|
|
HasMore: hasMore,
|
|
NextCursor: nextCursor,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func hit2AppDocument(hit es.Hit) (*searchEntity.ProjectDocument, error) {
|
|
doc := &searchEntity.ProjectDocument{}
|
|
|
|
if err := sonic.Unmarshal(hit.Source_, doc); err != nil {
|
|
return nil, err
|
|
}
|
|
return doc, nil
|
|
}
|
|
|
|
func fieldValueCaster(fieldName, cursor string) any {
|
|
switch fieldName {
|
|
case model.FieldOfCreateTime,
|
|
model.FieldOfUpdateTime,
|
|
model.FieldOfPublishTime,
|
|
model.FieldOfFavTime,
|
|
model.FieldOfRecentlyOpenTime:
|
|
cursorInt, err := strconv.ParseInt(cursor, 10, 64)
|
|
if err != nil {
|
|
cursorInt = 0
|
|
}
|
|
|
|
return cursorInt
|
|
default:
|
|
return cursor
|
|
}
|
|
}
|
|
|
|
func formatProjectNextCursor(ob string, val *searchEntity.ProjectDocument) string {
|
|
fieldName2Cursor := map[string]string{
|
|
model.FieldOfCreateTime: conv.Int64ToStr(val.GetCreateTime()),
|
|
model.FieldOfUpdateTime: conv.Int64ToStr(val.GetUpdateTime()),
|
|
model.FieldOfPublishTime: conv.Int64ToStr(val.GetPublishTime()),
|
|
model.FieldOfFavTime: conv.Int64ToStr(val.GetFavTime()),
|
|
model.FieldOfRecentlyOpenTime: conv.Int64ToStr(val.GetRecentlyOpenTime()),
|
|
}
|
|
|
|
res, ok := fieldName2Cursor[ob]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func formatResourceNextCursor(ob string, val *searchEntity.ResourceDocument) string {
|
|
fieldName2Cursor := map[string]string{
|
|
model.FieldOfCreateTime: conv.Int64ToStr(val.GetCreateTime()),
|
|
model.FieldOfUpdateTime: conv.Int64ToStr(val.GetUpdateTime()),
|
|
model.FieldOfPublishTime: conv.Int64ToStr(val.GetPublishTime()),
|
|
}
|
|
|
|
res, ok := fieldName2Cursor[ob]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (s *searchImpl) SearchResources(ctx context.Context, req *searchEntity.SearchResourcesRequest) (resp *searchEntity.SearchResourcesResponse, err error) {
|
|
logs.CtxDebugf(ctx, "[SearchResources] search : %s", conv.DebugJsonToStr(req))
|
|
searchReq := &es.Request{
|
|
Query: &es.Query{
|
|
Bool: &es.BoolQuery{},
|
|
},
|
|
}
|
|
|
|
if req.APPID > 0 {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfAPPID, conv.Int64ToStr(req.APPID)))
|
|
} else {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfSpaceID, conv.Int64ToStr(req.SpaceID)))
|
|
searchReq.Query.Bool.Should = append(searchReq.Query.Bool.Should,
|
|
es.NewNotExistsQuery(fieldOfAPPID))
|
|
searchReq.Query.Bool.Should = append(searchReq.Query.Bool.Should,
|
|
es.NewEqualQuery(fieldOfAPPID, "0"))
|
|
|
|
searchReq.Query.Bool.MinimumShouldMatch = ptr.Of(1)
|
|
}
|
|
|
|
if req.Name != "" {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewContainsQuery(fieldOfNameRaw, req.Name))
|
|
}
|
|
|
|
if req.OwnerID > 0 {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(fieldOfOwnerID, conv.Int64ToStr(req.OwnerID)))
|
|
}
|
|
|
|
if len(req.ResTypeFilter) == 1 && int(req.ResTypeFilter[0]) != resTypeSearchAll {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(searchEntity.FieldOfResType, req.ResTypeFilter[0]))
|
|
}
|
|
|
|
if len(req.ResTypeFilter) == 2 {
|
|
resType := req.ResTypeFilter[0]
|
|
resSubType := int(req.ResTypeFilter[1])
|
|
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(searchEntity.FieldOfResType, resType))
|
|
|
|
if resSubType != resTypeSearchAll {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(searchEntity.FieldOfResSubType, int(resSubType)))
|
|
}
|
|
}
|
|
|
|
if req.PublishStatusFilter != 0 {
|
|
searchReq.Query.Bool.Must = append(searchReq.Query.Bool.Must,
|
|
es.NewEqualQuery(searchEntity.FieldOfPublishStatus, req.PublishStatusFilter))
|
|
}
|
|
|
|
if req.OrderFiledName == "" {
|
|
req.OrderFiledName = model.FieldOfUpdateTime
|
|
}
|
|
|
|
searchReq.Sort = []es.SortFiled{
|
|
{
|
|
Field: req.OrderFiledName,
|
|
Asc: req.OrderAsc,
|
|
},
|
|
}
|
|
|
|
reqLimit := 100
|
|
if req.Limit > 0 {
|
|
reqLimit = int(req.Limit)
|
|
}
|
|
realLimit := reqLimit + 1
|
|
searchReq.Size = &realLimit
|
|
|
|
if req.Page != nil {
|
|
page := *req.Page
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
searchReq.From = ptr.Of(int(page-1) * reqLimit)
|
|
} else if req.Cursor != "" && req.Cursor != "0" {
|
|
searchReq.SearchAfter = []any{
|
|
req.Cursor,
|
|
}
|
|
}
|
|
|
|
result, err := s.esClient.Search(ctx, resourceIndexName, searchReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hits := result.Hits.Hits
|
|
|
|
hasMore := func() bool {
|
|
if len(hits) > reqLimit {
|
|
return true
|
|
}
|
|
return false
|
|
}()
|
|
|
|
if hasMore {
|
|
hits = hits[:reqLimit]
|
|
}
|
|
|
|
docs := make([]*searchEntity.ResourceDocument, 0, len(hits))
|
|
for _, hit := range hits {
|
|
doc := &searchEntity.ResourceDocument{}
|
|
if err = sonic.Unmarshal(hit.Source_, doc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
docs = append(docs, doc)
|
|
}
|
|
|
|
nextCursor := ""
|
|
if len(docs) > 0 {
|
|
nextCursor = formatResourceNextCursor(req.OrderFiledName, docs[len(docs)-1])
|
|
}
|
|
if nextCursor == "" {
|
|
hasMore = false
|
|
}
|
|
|
|
var total *int64
|
|
if result.Hits.Total != nil {
|
|
total = ptr.Of(result.Hits.Total.Value)
|
|
}
|
|
|
|
resp = &searchEntity.SearchResourcesResponse{
|
|
Data: docs,
|
|
TotalHits: total,
|
|
HasMore: hasMore,
|
|
NextCursor: nextCursor,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|