mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-08 05:47:07 +09:00
GO-4459: Add complex filters in search for query and object types
This commit is contained in:
parent
1c1b9da344
commit
a2ccc7da5e
7 changed files with 148 additions and 62 deletions
|
@ -30,7 +30,8 @@ var log = logging.Logger("rest-api")
|
|||
func ReplacePlaceholders(endpoint string, parameters map[string]interface{}) string {
|
||||
for key, value := range parameters {
|
||||
placeholder := fmt.Sprintf("{%s}", key)
|
||||
endpoint = strings.ReplaceAll(endpoint, placeholder, fmt.Sprintf("%v", value))
|
||||
encodedValue := url.QueryEscape(fmt.Sprintf("%v", value))
|
||||
endpoint = strings.ReplaceAll(endpoint, placeholder, encodedValue)
|
||||
}
|
||||
|
||||
// Parse the base URL + endpoint
|
||||
|
|
|
@ -122,9 +122,13 @@ const docTemplate = `{
|
|||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Specify object.Object type for search",
|
||||
"name": "object_type",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Specify object types for search",
|
||||
"name": "object_types",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -116,9 +116,13 @@
|
|||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Specify object.Object type for search",
|
||||
"name": "object_type",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "csv",
|
||||
"description": "Specify object types for search",
|
||||
"name": "object_types",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -386,10 +386,13 @@ paths:
|
|||
in: query
|
||||
name: query
|
||||
type: string
|
||||
- description: Specify object.Object type for search
|
||||
- collectionFormat: csv
|
||||
description: Specify object types for search
|
||||
in: query
|
||||
name: object_type
|
||||
type: string
|
||||
items:
|
||||
type: string
|
||||
name: object_types
|
||||
type: array
|
||||
- description: The number of items to skip before starting to collect the result
|
||||
set
|
||||
in: query
|
||||
|
|
|
@ -16,23 +16,23 @@ import (
|
|||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string false "The search term to filter objects by name"
|
||||
// @Param object_type query string false "Specify object.Object type for search"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]object.Object "List of objects"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Param query query string false "The search term to filter objects by name"
|
||||
// @Param object_types query []string false "Specify object types for search"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set"
|
||||
// @Param limit query int false "The number of items to return" default(100)
|
||||
// @Success 200 {object} map[string][]object.Object "List of objects"
|
||||
// @Failure 403 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 502 {object} util.ServerError "Internal server error"
|
||||
// @Router /search [get]
|
||||
func SearchHandler(s *SearchService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
searchQuery := c.Query("query")
|
||||
objectType := c.Query("object_type")
|
||||
objectTypes := c.QueryArray("object_types")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
objects, total, hasMore, err := s.Search(c, searchQuery, objectType, offset, limit)
|
||||
objects, total, hasMore, err := s.Search(c, searchQuery, objectTypes, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrNoObjectsFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedSearchObjects, http.StatusInternalServerError),
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anyproto/anytype-heart/cmd/api/object"
|
||||
"github.com/anyproto/anytype-heart/cmd/api/pagination"
|
||||
|
@ -22,7 +23,7 @@ var (
|
|||
)
|
||||
|
||||
type Service interface {
|
||||
Search(ctx context.Context, searchQuery string, objectType string, offset, limit int) (objects []object.Object, total int, hasMore bool, err error)
|
||||
Search(ctx context.Context, searchQuery string, objectTypes []string, offset, limit int) (objects []object.Object, total int, hasMore bool, err error)
|
||||
}
|
||||
|
||||
type SearchService struct {
|
||||
|
@ -37,51 +38,16 @@ func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceServic
|
|||
}
|
||||
|
||||
// Search retrieves a paginated list of objects from all spaces that match the search parameters.
|
||||
func (s *SearchService) Search(ctx context.Context, searchQuery string, objectType string, offset, limit int) (objects []object.Object, total int, hasMore bool, err error) {
|
||||
func (s *SearchService) Search(ctx context.Context, searchQuery string, objectTypes []string, offset, limit int) (objects []object.Object, total int, hasMore bool, err error) {
|
||||
spaces, _, _, err := s.spaceService.ListSpaces(ctx, 0, 100)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
// Then, get objects from each space that match the search parameters
|
||||
var filters = []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
}
|
||||
|
||||
if searchQuery != "" {
|
||||
// TODO also include snippet for notes
|
||||
filters = append(filters, &model.BlockContentDataviewFilter{
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
})
|
||||
}
|
||||
|
||||
if objectType != "" {
|
||||
filters = append(filters, &model.BlockContentDataviewFilter{
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(objectType),
|
||||
})
|
||||
}
|
||||
baseFilters := s.prepareBaseFilters()
|
||||
queryFilters := s.prepareQueryFilter(searchQuery)
|
||||
objectTypeFilters := s.prepareObjectTypeFilters(objectTypes)
|
||||
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, objectTypeFilters)
|
||||
|
||||
results := make([]object.Object, 0)
|
||||
for _, space := range spaces {
|
||||
|
@ -96,7 +62,7 @@ func (s *SearchService) Search(ctx context.Context, searchQuery string, objectTy
|
|||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{"id", "name", "type", "layout", "iconEmoji", "iconImage"},
|
||||
Keys: []string{"id", "name", "type", "snippet", "layout", "iconEmoji", "iconImage"},
|
||||
// TODO split limit between spaces
|
||||
// Limit: paginationLimitPerSpace,
|
||||
// FullText: searchTerm,
|
||||
|
@ -122,6 +88,7 @@ func (s *SearchService) Search(ctx context.Context, searchQuery string, objectTy
|
|||
ObjectId: record.Fields["id"].GetStringValue(),
|
||||
})
|
||||
|
||||
// TODO: return snippet for notes?
|
||||
results = append(results, object.Object{
|
||||
Type: model.ObjectTypeLayout_name[int32(record.Fields["layout"].GetNumberValue())],
|
||||
Id: record.Fields["id"].GetStringValue(),
|
||||
|
@ -150,3 +117,107 @@ func (s *SearchService) Search(ctx context.Context, searchQuery string, objectTy
|
|||
paginatedResults, hasMore := pagination.Paginate(results, offset, limit)
|
||||
return paginatedResults, total, hasMore, nil
|
||||
}
|
||||
|
||||
// makeAndCondition combines multiple filter groups with the given operator.
|
||||
func (s *SearchService) combineFilters(operator model.BlockContentDataviewFilterOperator, filterGroups ...[]*model.BlockContentDataviewFilter) []*model.BlockContentDataviewFilter {
|
||||
nestedFilters := make([]*model.BlockContentDataviewFilter, 0)
|
||||
for _, group := range filterGroups {
|
||||
nestedFilters = append(nestedFilters, group...)
|
||||
}
|
||||
|
||||
if len(nestedFilters) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: operator,
|
||||
NestedFilters: nestedFilters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareBaseFilters returns a list of default filters that should be applied to all search queries.
|
||||
func (s *SearchService) prepareBaseFilters() []*model.BlockContentDataviewFilter {
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
// int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareQueryFilter combines object name and snippet filters with an OR condition.
|
||||
func (s *SearchService) prepareQueryFilter(searchQuery string) []*model.BlockContentDataviewFilter {
|
||||
if searchQuery == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_Or,
|
||||
NestedFilters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySnippet.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareObjectTypeFilters combines object type filters with an OR condition.
|
||||
func (s *SearchService) prepareObjectTypeFilters(objectTypes []string) []*model.BlockContentDataviewFilter {
|
||||
if len(objectTypes) == 0 || objectTypes[0] == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare nested filters for each object type
|
||||
nestedFilters := make([]*model.BlockContentDataviewFilter, len(objectTypes))
|
||||
for i, objectType := range objectTypes {
|
||||
relationKey := bundle.RelationKeyType.String()
|
||||
if strings.HasPrefix(objectType, "ot-") {
|
||||
relationKey = bundle.RelationKeyUniqueKey.String()
|
||||
}
|
||||
|
||||
nestedFilters[i] = &model.BlockContentDataviewFilter{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: relationKey,
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(objectType),
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all filters with an OR operator
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_Or,
|
||||
NestedFilters: nestedFilters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,16 +48,19 @@ func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (s
|
|||
SpaceId: s.AccountInfo.TechSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySpaceRemoteStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue