github.com/weaviate/weaviate@v1.24.6/adapters/handlers/graphql/local/aggregate/properties.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/entities/aggregation"
    20  	"github.com/weaviate/weaviate/entities/models"
    21  )
    22  
    23  func numericPropertyFields(class *models.Class, property *models.Property, prefix string) *graphql.Object {
    24  	getMetaIntFields := graphql.Fields{
    25  		"sum": &graphql.Field{
    26  			Name:        fmt.Sprintf("%s%s%sSum", prefix, class.Class, property.Name),
    27  			Description: descriptions.AggregateSum,
    28  			Type:        graphql.Float,
    29  			Resolve:     makeResolveNumericFieldAggregator("sum"),
    30  		},
    31  		"minimum": &graphql.Field{
    32  			Name:        fmt.Sprintf("%s%s%sMinimum", prefix, class.Class, property.Name),
    33  			Description: descriptions.AggregateMin,
    34  			Type:        graphql.Float,
    35  			Resolve:     makeResolveNumericFieldAggregator("minimum"),
    36  		},
    37  		"maximum": &graphql.Field{
    38  			Name:        fmt.Sprintf("%s%s%sMaximum", prefix, class.Class, property.Name),
    39  			Description: descriptions.AggregateMax,
    40  			Type:        graphql.Float,
    41  			Resolve:     makeResolveNumericFieldAggregator("maximum"),
    42  		},
    43  		"mean": &graphql.Field{
    44  			Name:        fmt.Sprintf("%s%s%sMean", prefix, class.Class, property.Name),
    45  			Description: descriptions.AggregateMean,
    46  			Type:        graphql.Float,
    47  			Resolve:     makeResolveNumericFieldAggregator("mean"),
    48  		},
    49  		"mode": &graphql.Field{
    50  			Name:        fmt.Sprintf("%s%s%sMode", prefix, class.Class, property.Name),
    51  			Description: descriptions.AggregateMode,
    52  			Type:        graphql.Float,
    53  			Resolve:     makeResolveNumericFieldAggregator("mode"),
    54  		},
    55  		"median": &graphql.Field{
    56  			Name:        fmt.Sprintf("%s%s%sMedian", prefix, class.Class, property.Name),
    57  			Description: descriptions.AggregateMedian,
    58  			Type:        graphql.Float,
    59  			Resolve:     makeResolveNumericFieldAggregator("median"),
    60  		},
    61  		"count": &graphql.Field{
    62  			Name:        fmt.Sprintf("%s%s%sCount", prefix, class.Class, property.Name),
    63  			Description: descriptions.AggregateCount,
    64  			Type:        graphql.Int,
    65  			Resolve:     makeResolveNumericFieldAggregator("count"),
    66  		},
    67  		"type": &graphql.Field{
    68  			Name:        fmt.Sprintf("%s%s%sType", prefix, class.Class, property.Name),
    69  			Description: descriptions.AggregateCount,
    70  			Type:        graphql.String,
    71  			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
    72  				prop, ok := p.Source.(aggregation.Property)
    73  				if !ok {
    74  					return nil, fmt.Errorf("numerical: type: expected aggregation.Property, got %T", p.Source)
    75  				}
    76  
    77  				return prop.SchemaType, nil
    78  			},
    79  		},
    80  	}
    81  
    82  	return graphql.NewObject(graphql.ObjectConfig{
    83  		Name:        fmt.Sprintf("%s%s%sObj", prefix, class.Class, property.Name),
    84  		Fields:      getMetaIntFields,
    85  		Description: descriptions.AggregatePropertyObject,
    86  	})
    87  }
    88  
    89  func datePropertyFields(class *models.Class,
    90  	property *models.Property, prefix string,
    91  ) *graphql.Object {
    92  	getMetaDateFields := graphql.Fields{
    93  		"count": &graphql.Field{
    94  			Name:        fmt.Sprintf("%s%sCount", prefix, class.Class),
    95  			Description: descriptions.AggregateCount,
    96  			Type:        graphql.Int,
    97  			Resolve:     makeResolveDateFieldAggregator("count"),
    98  		},
    99  		"minimum": &graphql.Field{
   100  			Name:        fmt.Sprintf("%s%s%sMinimum", prefix, class.Class, property.Name),
   101  			Description: descriptions.AggregateMin,
   102  			Type:        graphql.String,
   103  			Resolve:     makeResolveDateFieldAggregator("minimum"),
   104  		},
   105  		"maximum": &graphql.Field{
   106  			Name:        fmt.Sprintf("%s%s%sMaximum", prefix, class.Class, property.Name),
   107  			Description: descriptions.AggregateMax,
   108  			Type:        graphql.String,
   109  			Resolve:     makeResolveDateFieldAggregator("maximum"),
   110  		},
   111  		"mode": &graphql.Field{
   112  			Name:        fmt.Sprintf("%s%s%sMode", prefix, class.Class, property.Name),
   113  			Description: descriptions.AggregateMode,
   114  			Type:        graphql.String,
   115  			Resolve:     makeResolveDateFieldAggregator("mode"),
   116  		},
   117  		"median": &graphql.Field{
   118  			Name:        fmt.Sprintf("%s%s%sMedian", prefix, class.Class, property.Name),
   119  			Description: descriptions.AggregateMedian,
   120  			Type:        graphql.String,
   121  			Resolve:     makeResolveDateFieldAggregator("median"),
   122  		},
   123  	}
   124  
   125  	return graphql.NewObject(graphql.ObjectConfig{
   126  		Name:        fmt.Sprintf("%s%s%sObj", prefix, class.Class, property.Name),
   127  		Fields:      getMetaDateFields,
   128  		Description: descriptions.AggregatePropertyObject,
   129  	})
   130  }
   131  
   132  func referencePropertyFields(class *models.Class,
   133  	property *models.Property, prefix string,
   134  ) *graphql.Object {
   135  	getMetaPointingFields := graphql.Fields{
   136  		"type": &graphql.Field{
   137  			Name:        fmt.Sprintf("%s%sType", prefix, class.Class),
   138  			Description: descriptions.AggregatePropertyType,
   139  			Type:        graphql.String,
   140  			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   141  				prop, ok := p.Source.(aggregation.Property)
   142  				if !ok {
   143  					return nil, fmt.Errorf("ref property type: expected aggregation.Property, got %T",
   144  						p.Source)
   145  				}
   146  
   147  				return prop.SchemaType, nil
   148  			},
   149  		},
   150  		"pointingTo": &graphql.Field{
   151  			Name:        fmt.Sprintf("%s%sPointingTo", prefix, class.Class),
   152  			Description: descriptions.AggregateClassPropertyPointingTo,
   153  			Type:        graphql.NewList(graphql.String),
   154  			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   155  				ref, err := extractReferenceAggregation(p.Source)
   156  				if err != nil {
   157  					return nil, fmt.Errorf("ref property pointingTo: %v", err)
   158  				}
   159  
   160  				return ref.PointingTo, nil
   161  			},
   162  			DeprecationReason: "Experimental, the format will change",
   163  		},
   164  	}
   165  
   166  	return graphql.NewObject(graphql.ObjectConfig{
   167  		Name:        fmt.Sprintf("%s%s%sObj", prefix, class.Class, property.Name),
   168  		Fields:      getMetaPointingFields,
   169  		Description: descriptions.AggregatePropertyObject,
   170  	})
   171  }
   172  
   173  func extractReferenceAggregation(source interface{}) (*aggregation.Reference, error) {
   174  	property, ok := source.(aggregation.Property)
   175  	if !ok {
   176  		return nil, fmt.Errorf("expected aggregation.Property, got %T", source)
   177  	}
   178  
   179  	if property.Type != aggregation.PropertyTypeReference {
   180  		return nil, fmt.Errorf("expected property to be of type reference, got %s", property.Type)
   181  	}
   182  
   183  	return &property.ReferenceAggregation, nil
   184  }
   185  
   186  func booleanPropertyFields(class *models.Class,
   187  	property *models.Property, prefix string,
   188  ) *graphql.Object {
   189  	getMetaPointingFields := graphql.Fields{
   190  		"count": &graphql.Field{
   191  			Name:        fmt.Sprintf("%s%s%sCount", prefix, class.Class, property.Name),
   192  			Description: descriptions.AggregatePropertyCount,
   193  			Type:        graphql.Int,
   194  			Resolve:     booleanResolver(func(b aggregation.Boolean) interface{} { return b.Count }),
   195  		},
   196  		"totalTrue": &graphql.Field{
   197  			Name:        fmt.Sprintf("%s%s%sTotalTrue", prefix, class.Class, property.Name),
   198  			Description: descriptions.AggregateClassPropertyTotalTrue,
   199  			Type:        graphql.Int,
   200  			Resolve:     booleanResolver(func(b aggregation.Boolean) interface{} { return b.TotalTrue }),
   201  		},
   202  		"percentageTrue": &graphql.Field{
   203  			Name:        fmt.Sprintf("%s%s%sPercentageTrue", prefix, class.Class, property.Name),
   204  			Description: descriptions.AggregateClassPropertyPercentageTrue,
   205  			Type:        graphql.Float,
   206  			Resolve:     booleanResolver(func(b aggregation.Boolean) interface{} { return b.PercentageTrue }),
   207  		},
   208  		"totalFalse": &graphql.Field{
   209  			Name:        fmt.Sprintf("%s%s%sTotalFalse", prefix, class.Class, property.Name),
   210  			Description: descriptions.AggregateClassPropertyTotalFalse,
   211  			Type:        graphql.Int,
   212  			Resolve:     booleanResolver(func(b aggregation.Boolean) interface{} { return b.TotalFalse }),
   213  		},
   214  		"percentageFalse": &graphql.Field{
   215  			Name:        fmt.Sprintf("%s%s%sPercentageFalse", prefix, class.Class, property.Name),
   216  			Description: descriptions.AggregateClassPropertyPercentageFalse,
   217  			Type:        graphql.Float,
   218  			Resolve:     booleanResolver(func(b aggregation.Boolean) interface{} { return b.PercentageFalse }),
   219  		},
   220  		"type": &graphql.Field{
   221  			Name:        fmt.Sprintf("%s%s%sType", prefix, class.Class, property.Name),
   222  			Description: descriptions.AggregateCount,
   223  			Type:        graphql.String,
   224  			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   225  				prop, ok := p.Source.(aggregation.Property)
   226  				if !ok {
   227  					return nil, fmt.Errorf("boolean: type: expected aggregation.Property, got %T", p.Source)
   228  				}
   229  
   230  				return prop.SchemaType, nil
   231  			},
   232  		},
   233  	}
   234  
   235  	return graphql.NewObject(graphql.ObjectConfig{
   236  		Name:        fmt.Sprintf("%s%s%sObj", prefix, class.Class, property.Name),
   237  		Fields:      getMetaPointingFields,
   238  		Description: descriptions.AggregatePropertyObject,
   239  	})
   240  }
   241  
   242  type booleanExtractorFunc func(aggregation.Boolean) interface{}
   243  
   244  func booleanResolver(extractor booleanExtractorFunc) func(p graphql.ResolveParams) (interface{}, error) {
   245  	return func(p graphql.ResolveParams) (interface{}, error) {
   246  		boolean, err := extractBooleanAggregation(p.Source)
   247  		if err != nil {
   248  			return nil, fmt.Errorf("boolean: %v", err)
   249  		}
   250  
   251  		return extractor(*boolean), nil
   252  	}
   253  }
   254  
   255  func extractBooleanAggregation(source interface{}) (*aggregation.Boolean, error) {
   256  	property, ok := source.(aggregation.Property)
   257  	if !ok {
   258  		return nil, fmt.Errorf("expected aggregation.Property, got %T", source)
   259  	}
   260  
   261  	if property.Type != aggregation.PropertyTypeBoolean {
   262  		return nil, fmt.Errorf("expected property to be of type boolean, got %s", property.Type)
   263  	}
   264  
   265  	return &property.BooleanAggregation, nil
   266  }
   267  
   268  func stringPropertyFields(class *models.Class,
   269  	property *models.Property, prefix string,
   270  ) *graphql.Object {
   271  	getAggregatePointingFields := graphql.Fields{
   272  		"count": &graphql.Field{
   273  			Name:        fmt.Sprintf("%s%sCount", prefix, class.Class),
   274  			Description: descriptions.AggregatePropertyCount,
   275  			Type:        graphql.Int,
   276  			Resolve: textResolver(func(text aggregation.Text) (interface{}, error) {
   277  				return text.Count, nil
   278  			}),
   279  		},
   280  		"type": &graphql.Field{
   281  			Name:        fmt.Sprintf("%s%s%sType", prefix, class.Class, property.Name),
   282  			Description: descriptions.AggregateCount,
   283  			Type:        graphql.String,
   284  			Resolve: func(p graphql.ResolveParams) (interface{}, error) {
   285  				prop, ok := p.Source.(aggregation.Property)
   286  				if !ok {
   287  					return nil, fmt.Errorf("text type: expected aggregation.Property, got %T", p.Source)
   288  				}
   289  
   290  				return prop.SchemaType, nil
   291  			},
   292  		},
   293  		"topOccurrences": &graphql.Field{
   294  			Name:        fmt.Sprintf("%s%sTopOccurrences", prefix, class.Class),
   295  			Description: descriptions.AggregatePropertyTopOccurrences,
   296  			Type:        graphql.NewList(stringTopOccurrences(class, property, prefix)),
   297  			Resolve: textResolver(func(text aggregation.Text) (interface{}, error) {
   298  				list := make([]interface{}, len(text.Items))
   299  				for i, to := range text.Items {
   300  					list[i] = to
   301  				}
   302  
   303  				return list, nil
   304  			}),
   305  			Args: graphql.FieldConfigArgument{
   306  				"limit": &graphql.ArgumentConfig{
   307  					Description: descriptions.First,
   308  					Type:        graphql.Int,
   309  				},
   310  			},
   311  		},
   312  	}
   313  
   314  	return graphql.NewObject(graphql.ObjectConfig{
   315  		Name:        fmt.Sprintf("%s%s%sObj", prefix, class.Class, property.Name),
   316  		Fields:      getAggregatePointingFields,
   317  		Description: descriptions.AggregatePropertyObject,
   318  	})
   319  }
   320  
   321  type textExtractorFunc func(aggregation.Text) (interface{}, error)
   322  
   323  func textResolver(extractor textExtractorFunc) func(p graphql.ResolveParams) (interface{}, error) {
   324  	return func(p graphql.ResolveParams) (interface{}, error) {
   325  		text, err := extractTextAggregation(p.Source)
   326  		if err != nil {
   327  			return nil, fmt.Errorf("text: %v", err)
   328  		}
   329  
   330  		return extractor(text)
   331  	}
   332  }
   333  
   334  func stringTopOccurrences(class *models.Class,
   335  	property *models.Property, prefix string,
   336  ) *graphql.Object {
   337  	getAggregateAggregatePointingFields := graphql.Fields{
   338  		"value": &graphql.Field{
   339  			Name:        fmt.Sprintf("%s%s%sTopOccurrencesValue", prefix, class.Class, property.Name),
   340  			Description: descriptions.AggregatePropertyTopOccurrencesValue,
   341  			Type:        graphql.String,
   342  			Resolve:     textOccurrenceResolver(func(t aggregation.TextOccurrence) interface{} { return t.Value }),
   343  		},
   344  		"occurs": &graphql.Field{
   345  			Name:        fmt.Sprintf("%s%s%sTopOccurrencesOccurs", prefix, class.Class, property.Name),
   346  			Description: descriptions.AggregatePropertyTopOccurrencesOccurs,
   347  			Type:        graphql.Int,
   348  			Resolve:     textOccurrenceResolver(func(t aggregation.TextOccurrence) interface{} { return t.Occurs }),
   349  		},
   350  	}
   351  
   352  	getAggregateAggregatePointing := graphql.ObjectConfig{
   353  		Name:        fmt.Sprintf("%s%s%sTopOccurrencesObj", prefix, class.Class, property.Name),
   354  		Fields:      getAggregateAggregatePointingFields,
   355  		Description: descriptions.AggregatePropertyTopOccurrences,
   356  	}
   357  
   358  	return graphql.NewObject(getAggregateAggregatePointing)
   359  }
   360  
   361  type textOccurrenceExtractorFunc func(aggregation.TextOccurrence) interface{}
   362  
   363  func textOccurrenceResolver(extractor textOccurrenceExtractorFunc) func(p graphql.ResolveParams) (interface{}, error) {
   364  	return func(p graphql.ResolveParams) (interface{}, error) {
   365  		textOccurrence, ok := p.Source.(aggregation.TextOccurrence)
   366  		if !ok {
   367  			return nil, fmt.Errorf("textOccurrence: %s: expected aggregation.TextOccurrence, but got %T",
   368  				p.Info.FieldName, p.Source)
   369  		}
   370  
   371  		return extractor(textOccurrence), nil
   372  	}
   373  }
   374  
   375  func extractTextAggregation(source interface{}) (aggregation.Text, error) {
   376  	property, ok := source.(aggregation.Property)
   377  	if !ok {
   378  		return aggregation.Text{}, fmt.Errorf("expected aggregation.Property, got %T", source)
   379  	}
   380  
   381  	if property.Type == aggregation.PropertyTypeNumerical {
   382  		// in this case we can only use count
   383  		return aggregation.Text{
   384  			Count: property.NumericalAggregations["count"].(int),
   385  		}, nil
   386  	}
   387  
   388  	if property.Type != aggregation.PropertyTypeText {
   389  		return aggregation.Text{}, fmt.Errorf("expected property to be of type text, got %s (%#v)", property.Type, property)
   390  	}
   391  
   392  	return property.TextAggregation, nil
   393  }
   394  
   395  func groupedByProperty(class *models.Class) *graphql.Object {
   396  	classProperties := graphql.Fields{
   397  		"path": &graphql.Field{
   398  			Description: descriptions.AggregateGroupedByGroupedByPath,
   399  			Type:        graphql.NewList(graphql.String),
   400  			Resolve:     groupedByResolver(func(g *aggregation.GroupedBy) interface{} { return g.Path }),
   401  		},
   402  		"value": &graphql.Field{
   403  			Description: descriptions.AggregateGroupedByGroupedByValue,
   404  			Type:        graphql.String,
   405  			Resolve:     groupedByResolver(func(g *aggregation.GroupedBy) interface{} { return g.Value }),
   406  		},
   407  	}
   408  
   409  	classPropertiesObj := graphql.NewObject(graphql.ObjectConfig{
   410  		Name:        fmt.Sprintf("Aggregate%sGroupedByObj", class.Class),
   411  		Fields:      classProperties,
   412  		Description: descriptions.AggregateGroupedByObj,
   413  	})
   414  
   415  	return classPropertiesObj
   416  }
   417  
   418  type groupedByExtractorFunc func(*aggregation.GroupedBy) interface{}
   419  
   420  func groupedByResolver(extractor groupedByExtractorFunc) func(p graphql.ResolveParams) (interface{}, error) {
   421  	return func(p graphql.ResolveParams) (interface{}, error) {
   422  		groupedBy, ok := p.Source.(*aggregation.GroupedBy)
   423  		if !ok {
   424  			return nil, fmt.Errorf("groupedBy: %s: expected aggregation.GroupedBy, but got %T",
   425  				p.Info.FieldName, p.Source)
   426  		}
   427  
   428  		return extractor(groupedBy), nil
   429  	}
   430  }
   431  
   432  func makeResolveNumericFieldAggregator(aggregator string) func(p graphql.ResolveParams) (interface{}, error) {
   433  	return func(p graphql.ResolveParams) (interface{}, error) {
   434  		num, err := extractNumericAggregation(p.Source)
   435  		if err != nil {
   436  			return nil, fmt.Errorf("numerical aggregator %s: %v", aggregator, err)
   437  		}
   438  
   439  		return num[aggregator], nil
   440  	}
   441  }
   442  
   443  func extractNumericAggregation(source interface{}) (map[string]interface{}, error) {
   444  	property, ok := source.(aggregation.Property)
   445  	if !ok {
   446  		return nil, fmt.Errorf("expected aggregation.Property, got %T", source)
   447  	}
   448  
   449  	if property.Type != aggregation.PropertyTypeNumerical {
   450  		return nil, fmt.Errorf("expected property to be of type numerical, got %s", property.Type)
   451  	}
   452  
   453  	return property.NumericalAggregations, nil
   454  }
   455  
   456  func makeResolveDateFieldAggregator(aggregator string) func(p graphql.ResolveParams) (interface{}, error) {
   457  	return func(p graphql.ResolveParams) (interface{}, error) {
   458  		date, err := extractDateAggregation(p.Source)
   459  		if err != nil {
   460  			return nil, fmt.Errorf("date aggregator %s: %v", aggregator, err)
   461  		}
   462  
   463  		return date[aggregator], nil
   464  	}
   465  }
   466  
   467  func extractDateAggregation(source interface{}) (map[string]interface{}, error) {
   468  	property, ok := source.(aggregation.Property)
   469  	if !ok {
   470  		return nil, fmt.Errorf("expected aggregation.Property, got %T", source)
   471  	}
   472  
   473  	if property.Type != aggregation.PropertyTypeDate {
   474  		return nil, fmt.Errorf("expected property to be of type date, got %s", property.Type)
   475  	}
   476  
   477  	return property.DateAggregations, nil
   478  }