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
 | |
| }
 |