1
0
Fork 0
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:
Jannis Metrikat 2025-01-02 14:14:07 +01:00
parent 1c1b9da344
commit a2ccc7da5e
No known key found for this signature in database
GPG key ID: B223CAC5AAF85615
7 changed files with 148 additions and 62 deletions

View file

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

View file

@ -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"
},
{

View file

@ -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"
},
{

View file

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

View file

@ -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),

View file

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

View file

@ -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)),