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 { // 精确搜索
 | 
						|
		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
 | 
						|
}
 |