coze-studio/backend/domain/search/service/search.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
}