github.com/weaviate/weaviate@v1.24.6/adapters/handlers/graphql/local/aggregate/aggregate.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package aggregate
    13  
    14  import (
    15  	"fmt"
    16  
    17  	"github.com/tailor-inc/graphql"
    18  	"github.com/weaviate/weaviate/adapters/handlers/graphql/descriptions"
    19  	"github.com/weaviate/weaviate/adapters/handlers/graphql/local/common_filters"
    20  	"github.com/weaviate/weaviate/adapters/handlers/graphql/utils"
    21  	"github.com/weaviate/weaviate/entities/aggregation"
    22  	"github.com/weaviate/weaviate/entities/models"
    23  	"github.com/weaviate/weaviate/entities/schema"
    24  	"github.com/weaviate/weaviate/usecases/config"
    25  )
    26  
    27  type ModulesProvider interface {
    28  	AggregateArguments(class *models.Class) map[string]*graphql.ArgumentConfig
    29  	ExtractSearchParams(arguments map[string]interface{}, className string) map[string]interface{}
    30  }
    31  
    32  // Build the Aggregate Kinds schema
    33  func Build(dbSchema *schema.Schema, config config.Config,
    34  	modulesProvider ModulesProvider,
    35  ) (*graphql.Field, error) {
    36  	if len(dbSchema.Objects.Classes) == 0 {
    37  		return nil, utils.ErrEmptySchema
    38  	}
    39  
    40  	var err error
    41  	var localAggregateObjects *graphql.Object
    42  	if len(dbSchema.Objects.Classes) > 0 {
    43  		localAggregateObjects, err = classFields(dbSchema.Objects.Classes, config, modulesProvider)
    44  		if err != nil {
    45  			return nil, err
    46  		}
    47  	}
    48  
    49  	field := graphql.Field{
    50  		Name:        "Aggregate",
    51  		Description: descriptions.AggregateWhere,
    52  		Type:        localAggregateObjects,
    53  		Resolve:     passThroughResolver,
    54  	}
    55  
    56  	return &field, nil
    57  }
    58  
    59  func classFields(databaseSchema []*models.Class,
    60  	config config.Config, modulesProvider ModulesProvider,
    61  ) (*graphql.Object, error) {
    62  	fields := graphql.Fields{}
    63  
    64  	for _, class := range databaseSchema {
    65  		field, err := classField(class, class.Description, config, modulesProvider)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  
    70  		fields[class.Class] = field
    71  	}
    72  
    73  	return graphql.NewObject(graphql.ObjectConfig{
    74  		Name:        "AggregateObjectsObj",
    75  		Fields:      fields,
    76  		Description: descriptions.AggregateObjectsObj,
    77  	}), nil
    78  }
    79  
    80  func classField(class *models.Class, description string,
    81  	config config.Config, modulesProvider ModulesProvider,
    82  ) (*graphql.Field, error) {
    83  	metaClassName := fmt.Sprintf("Aggregate%s", class.Class)
    84  
    85  	fields := graphql.ObjectConfig{
    86  		Name: metaClassName,
    87  		Fields: (graphql.FieldsThunk)(func() graphql.Fields {
    88  			fields, err := classPropertyFields(class)
    89  			if err != nil {
    90  				// we cannot return an error in this FieldsThunk and have to panic unfortunately
    91  				panic(fmt.Sprintf("Failed to assemble single Local Aggregate Class field: %s", err))
    92  			}
    93  
    94  			return fields
    95  		}),
    96  		Description: description,
    97  	}
    98  
    99  	fieldsObject := graphql.NewObject(fields)
   100  	fieldsField := &graphql.Field{
   101  		Type:        graphql.NewList(fieldsObject),
   102  		Description: description,
   103  		Args: graphql.FieldConfigArgument{
   104  			"limit": &graphql.ArgumentConfig{
   105  				Description: descriptions.First,
   106  				Type:        graphql.Int,
   107  			},
   108  			"where": &graphql.ArgumentConfig{
   109  				Description: descriptions.GetWhere,
   110  				Type: graphql.NewInputObject(
   111  					graphql.InputObjectConfig{
   112  						Name:        fmt.Sprintf("AggregateObjects%sWhereInpObj", class.Class),
   113  						Fields:      common_filters.BuildNew(fmt.Sprintf("AggregateObjects%s", class.Class)),
   114  						Description: descriptions.GetWhereInpObj,
   115  					},
   116  				),
   117  			},
   118  			"groupBy": &graphql.ArgumentConfig{
   119  				Description: descriptions.GroupBy,
   120  				Type:        graphql.NewList(graphql.String),
   121  			},
   122  			"nearVector": nearVectorArgument(class.Class),
   123  			"nearObject": nearObjectArgument(class.Class),
   124  			"objectLimit": &graphql.ArgumentConfig{
   125  				Description: descriptions.First,
   126  				Type:        graphql.Int,
   127  			},
   128  			"hybrid": hybridArgument(fieldsObject, class, modulesProvider),
   129  		},
   130  		Resolve: makeResolveClass(modulesProvider, class),
   131  	}
   132  
   133  	if modulesProvider != nil {
   134  		for name, argument := range modulesProvider.AggregateArguments(class) {
   135  			fieldsField.Args[name] = argument
   136  		}
   137  	}
   138  
   139  	if schema.MultiTenancyEnabled(class) {
   140  		fieldsField.Args["tenant"] = tenantArgument()
   141  	}
   142  
   143  	return fieldsField, nil
   144  }
   145  
   146  func classPropertyFields(class *models.Class) (graphql.Fields, error) {
   147  	fields := graphql.Fields{}
   148  	for _, property := range class.Properties {
   149  		propertyType, err := schema.GetPropertyDataType(class, property.Name)
   150  		if err != nil {
   151  			return nil, fmt.Errorf("%s.%s: %s", class.Class, property.Name, err)
   152  		}
   153  
   154  		convertedDataType, err := classPropertyField(*propertyType, class, property)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  
   159  		fields[property.Name] = convertedDataType
   160  	}
   161  
   162  	// Special case: meta { count } appended to all regular props
   163  	fields["meta"] = &graphql.Field{
   164  		Description: descriptions.LocalMetaObj,
   165  		Type:        metaObject(fmt.Sprintf("Aggregate%s", class.Class)),
   166  		Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   167  			// pass-through
   168  			return p.Source, nil
   169  		},
   170  	}
   171  
   172  	// Always append Grouped By field
   173  	fields["groupedBy"] = &graphql.Field{
   174  		Description: descriptions.AggregateGroupedBy,
   175  		Type:        groupedByProperty(class),
   176  		Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   177  			switch typed := p.Source.(type) {
   178  			case aggregation.Group:
   179  				return typed.GroupedBy, nil
   180  			case map[string]interface{}:
   181  				return typed["groupedBy"], nil
   182  			default:
   183  				return nil, fmt.Errorf("groupedBy: unsupported type %T", p.Source)
   184  			}
   185  		},
   186  	}
   187  
   188  	return fields, nil
   189  }
   190  
   191  func metaObject(prefix string) *graphql.Object {
   192  	return graphql.NewObject(graphql.ObjectConfig{
   193  		Name: fmt.Sprintf("%sMetaObject", prefix),
   194  		Fields: graphql.Fields{
   195  			"count": &graphql.Field{
   196  				Type: graphql.Int,
   197  				Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   198  					group, ok := p.Source.(aggregation.Group)
   199  					if !ok {
   200  						return nil, fmt.Errorf("meta count: expected aggregation.Group, got %T", p.Source)
   201  					}
   202  
   203  					return group.Count, nil
   204  				},
   205  			},
   206  		},
   207  	})
   208  }
   209  
   210  func classPropertyField(dataType schema.DataType, class *models.Class, property *models.Property) (*graphql.Field, error) {
   211  	switch dataType {
   212  	case schema.DataTypeText:
   213  		return makePropertyField(class, property, stringPropertyFields)
   214  	case schema.DataTypeInt:
   215  		return makePropertyField(class, property, numericPropertyFields)
   216  	case schema.DataTypeNumber:
   217  		return makePropertyField(class, property, numericPropertyFields)
   218  	case schema.DataTypeBoolean:
   219  		return makePropertyField(class, property, booleanPropertyFields)
   220  	case schema.DataTypeDate:
   221  		return makePropertyField(class, property, datePropertyFields)
   222  	case schema.DataTypeCRef:
   223  		return makePropertyField(class, property, referencePropertyFields)
   224  	case schema.DataTypeGeoCoordinates:
   225  		// simply skip for now, see gh-729
   226  		return nil, nil
   227  	case schema.DataTypePhoneNumber:
   228  		// skipping for now, see gh-1088 where it was outscoped
   229  		return nil, nil
   230  	case schema.DataTypeBlob:
   231  		return makePropertyField(class, property, stringPropertyFields)
   232  	case schema.DataTypeTextArray:
   233  		return makePropertyField(class, property, stringPropertyFields)
   234  	case schema.DataTypeIntArray, schema.DataTypeNumberArray:
   235  		return makePropertyField(class, property, numericPropertyFields)
   236  	case schema.DataTypeBooleanArray:
   237  		return makePropertyField(class, property, booleanPropertyFields)
   238  	case schema.DataTypeDateArray:
   239  		return makePropertyField(class, property, datePropertyFields)
   240  	case schema.DataTypeUUID, schema.DataTypeUUIDArray:
   241  		// not aggregatable
   242  		return nil, nil
   243  	case schema.DataTypeObject, schema.DataTypeObjectArray:
   244  		// TODO: check if it's aggregable, skip for now
   245  		return nil, nil
   246  	default:
   247  		return nil, fmt.Errorf(schema.ErrorNoSuchDatatype+": %s", dataType)
   248  	}
   249  }
   250  
   251  type propertyFieldMaker func(class *models.Class,
   252  	property *models.Property, prefix string) *graphql.Object
   253  
   254  func makePropertyField(class *models.Class, property *models.Property,
   255  	fieldMaker propertyFieldMaker,
   256  ) (*graphql.Field, error) {
   257  	prefix := "Aggregate"
   258  	return &graphql.Field{
   259  		Description: fmt.Sprintf(`%s"%s"`, descriptions.AggregateProperty, property.Name),
   260  		Type:        fieldMaker(class, property, prefix),
   261  		Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   262  			switch typed := p.Source.(type) {
   263  			case aggregation.Group:
   264  				res, ok := typed.Properties[property.Name]
   265  				if !ok {
   266  					return nil, fmt.Errorf("missing property '%s'", property.Name)
   267  				}
   268  
   269  				return res, nil
   270  
   271  			default:
   272  				return nil, fmt.Errorf("property %s, unsupported type %T", property.Name, p.Source)
   273  			}
   274  		},
   275  	}, nil
   276  }
   277  
   278  func passThroughResolver(p graphql.ResolveParams) (interface{}, error) {
   279  	// bubble up root resolver
   280  	return p.Source, nil
   281  }