github.com/weaviate/weaviate@v1.24.6/adapters/handlers/grpc/v1/prepare_reply.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 v1
    13  
    14  import (
    15  	"fmt"
    16  	"math/big"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/weaviate/weaviate/usecases/byteops"
    21  
    22  	"github.com/weaviate/weaviate/entities/schema"
    23  	generative "github.com/weaviate/weaviate/usecases/modulecomponents/additional/generate"
    24  	"github.com/weaviate/weaviate/usecases/modulecomponents/additional/models"
    25  
    26  	"github.com/go-openapi/strfmt"
    27  	"github.com/pkg/errors"
    28  	"github.com/weaviate/weaviate/entities/additional"
    29  	"github.com/weaviate/weaviate/entities/dto"
    30  	"github.com/weaviate/weaviate/entities/search"
    31  	pb "github.com/weaviate/weaviate/grpc/generated/protocol/v1"
    32  	"google.golang.org/protobuf/types/known/structpb"
    33  )
    34  
    35  func searchResultsToProto(res []interface{}, start time.Time, searchParams dto.GetParams, scheme schema.Schema, usesPropertiesMessage bool) (*pb.SearchReply, error) {
    36  	tookSeconds := float64(time.Since(start)) / float64(time.Second)
    37  	out := &pb.SearchReply{
    38  		Took:                    float32(tookSeconds),
    39  		GenerativeGroupedResult: new(string), // pointer to empty string
    40  	}
    41  
    42  	if searchParams.GroupBy != nil {
    43  		out.GroupByResults = make([]*pb.GroupByResult, len(res))
    44  		for i, raw := range res {
    45  			group, generativeGroupResponse, err := extractGroup(raw, searchParams, scheme, usesPropertiesMessage)
    46  			if err != nil {
    47  				return nil, err
    48  			}
    49  			if generativeGroupResponse != "" {
    50  				out.GenerativeGroupedResult = &generativeGroupResponse
    51  			}
    52  			out.GroupByResults[i] = group
    53  		}
    54  	} else {
    55  		objects, generativeGroupResponse, err := extractObjectsToResults(res, searchParams, scheme, false, usesPropertiesMessage)
    56  		if err != nil {
    57  			return nil, err
    58  		}
    59  		out.GenerativeGroupedResult = &generativeGroupResponse
    60  		out.Results = objects
    61  	}
    62  	return out, nil
    63  }
    64  
    65  func extractObjectsToResults(res []interface{}, searchParams dto.GetParams, scheme schema.Schema, fromGroup, usesPropertiesMessage bool) ([]*pb.SearchResult, string, error) {
    66  	results := make([]*pb.SearchResult, len(res))
    67  	generativeGroupResultsReturn := ""
    68  	for i, raw := range res {
    69  		asMap, ok := raw.(map[string]interface{})
    70  		if !ok {
    71  			return nil, "", fmt.Errorf("could not parse returns %v", raw)
    72  		}
    73  		firstObject := i == 0
    74  
    75  		var props *pb.PropertiesResult
    76  		var err error
    77  
    78  		if usesPropertiesMessage {
    79  			props, err = extractPropertiesAnswer(scheme, asMap, searchParams.Properties, searchParams.ClassName, searchParams.AdditionalProperties)
    80  		} else {
    81  			props, err = extractPropertiesAnswerDeprecated(scheme, asMap, searchParams.Properties, searchParams.ClassName, searchParams.AdditionalProperties)
    82  		}
    83  		if err != nil {
    84  			return nil, "", err
    85  		}
    86  
    87  		additionalProps, generativeGroupResults, err := extractAdditionalProps(asMap, searchParams.AdditionalProperties, firstObject, fromGroup)
    88  		if err != nil {
    89  			return nil, "", err
    90  		}
    91  
    92  		if generativeGroupResultsReturn == "" && generativeGroupResults != "" {
    93  			generativeGroupResultsReturn = generativeGroupResults
    94  		}
    95  
    96  		result := &pb.SearchResult{
    97  			Properties: props,
    98  			Metadata:   additionalProps,
    99  		}
   100  
   101  		results[i] = result
   102  	}
   103  	return results, generativeGroupResultsReturn, nil
   104  }
   105  
   106  func idToByte(idRaw interface{}) ([]byte, string, error) {
   107  	idStrfmt, ok := idRaw.(strfmt.UUID)
   108  	if !ok {
   109  		return nil, "", errors.New("could not extract format id in additional prop")
   110  	}
   111  	idStrfmtStr := idStrfmt.String()
   112  	hexInteger, success := new(big.Int).SetString(strings.Replace(idStrfmtStr, "-", "", -1), 16)
   113  	if !success {
   114  		return nil, "", fmt.Errorf("failed to parse hex string to integer")
   115  	}
   116  	return hexInteger.Bytes(), idStrfmtStr, nil
   117  }
   118  
   119  func extractAdditionalProps(asMap map[string]any, additionalPropsParams additional.Properties, firstObject, fromGroup bool) (*pb.MetadataResult, string, error) {
   120  	generativeSearchRaw, generativeSearchEnabled := additionalPropsParams.ModuleParams["generate"]
   121  	_, rerankEnabled := additionalPropsParams.ModuleParams["rerank"]
   122  
   123  	metadata := &pb.MetadataResult{}
   124  	if additionalPropsParams.ID && !generativeSearchEnabled && !rerankEnabled && !fromGroup {
   125  		idRaw, ok := asMap["id"]
   126  		if !ok {
   127  			return nil, "", errors.New("could not extract get id in additional prop")
   128  		}
   129  
   130  		idToBytes, idAsString, err := idToByte(idRaw)
   131  		if err != nil {
   132  			return nil, "", errors.Wrap(err, "could not extract format id in additional prop")
   133  		}
   134  		metadata.Id = idAsString
   135  		metadata.IdAsBytes = idToBytes
   136  	}
   137  	_, ok := asMap["_additional"]
   138  	if !ok {
   139  		return metadata, "", nil
   140  	}
   141  
   142  	var additionalPropertiesMap map[string]interface{}
   143  	if !fromGroup {
   144  		additionalPropertiesMap = asMap["_additional"].(map[string]interface{})
   145  	} else {
   146  		addPropertiesGroup := asMap["_additional"].(*additional.GroupHitAdditional)
   147  		additionalPropertiesMap = make(map[string]interface{}, 3)
   148  		additionalPropertiesMap["id"] = addPropertiesGroup.ID
   149  		additionalPropertiesMap["vector"] = addPropertiesGroup.Vector
   150  		additionalPropertiesMap["distance"] = addPropertiesGroup.Distance
   151  	}
   152  	generativeGroupResults := ""
   153  	// id is part of the _additional map in case of generative search, group, & rerank - don't aks me why
   154  	if additionalPropsParams.ID && (generativeSearchEnabled || fromGroup || rerankEnabled) {
   155  		idRaw, ok := additionalPropertiesMap["id"]
   156  		if !ok {
   157  			return nil, "", errors.New("could not extract get id generative in additional prop")
   158  		}
   159  
   160  		idToBytes, idAsString, err := idToByte(idRaw)
   161  		if err != nil {
   162  			return nil, "", errors.Wrap(err, "could not extract format id in additional prop")
   163  		}
   164  		metadata.Id = idAsString
   165  		metadata.IdAsBytes = idToBytes
   166  	}
   167  
   168  	if generativeSearchEnabled {
   169  		var generateFmt *models.GenerateResult
   170  
   171  		generate, ok := additionalPropertiesMap["generate"]
   172  		if !ok {
   173  			generateFmt = &models.GenerateResult{}
   174  		} else {
   175  			generateFmt, ok = generate.(*models.GenerateResult)
   176  			if !ok {
   177  				return nil, "", errors.New("could not cast generative result additional prop")
   178  			}
   179  		}
   180  
   181  		if generateFmt.Error != nil {
   182  			return nil, "", generateFmt.Error
   183  		}
   184  
   185  		generativeSearch, ok := generativeSearchRaw.(*generative.Params)
   186  		if !ok {
   187  			return nil, "", errors.New("could not cast generative search params")
   188  		}
   189  		if generativeSearch.Prompt != nil && generateFmt.SingleResult == nil {
   190  			return nil, "", errors.New("No results for generative search despite a search request. Is a generative module enabled?")
   191  		}
   192  
   193  		if generateFmt.Error != nil {
   194  			return nil, "", generateFmt.Error
   195  		}
   196  
   197  		if generateFmt.SingleResult != nil && *generateFmt.SingleResult != "" {
   198  			metadata.Generative = *generateFmt.SingleResult
   199  			metadata.GenerativePresent = true
   200  		}
   201  
   202  		// grouped results are only added to the first object for GQL reasons
   203  		// however, reranking can result in a different order, so we need to check every object
   204  		// recording the result if it's present assuming that it is at least somewhere and will be caught
   205  		if generateFmt.GroupedResult != nil && *generateFmt.GroupedResult != "" {
   206  			generativeGroupResults = *generateFmt.GroupedResult
   207  		}
   208  	}
   209  
   210  	if rerankEnabled {
   211  		rerank, ok := additionalPropertiesMap["rerank"]
   212  		if !ok {
   213  			return nil, "", errors.New("No results for rerank despite a search request. Is a the rerank module enabled?")
   214  		}
   215  		rerankFmt, ok := rerank.([]*models.RankResult)
   216  		if !ok {
   217  			return nil, "", errors.New("could not cast rerank result additional prop")
   218  		}
   219  		metadata.RerankScore = *rerankFmt[0].Score
   220  		metadata.RerankScorePresent = true
   221  	}
   222  
   223  	// additional properties are only present for certain searches/configs => don't return an error if not available
   224  	if additionalPropsParams.Vector {
   225  		vector, ok := additionalPropertiesMap["vector"]
   226  		if ok {
   227  			vectorfmt, ok2 := vector.([]float32)
   228  			if ok2 {
   229  				metadata.Vector = vectorfmt // deprecated, remove in a bit
   230  				metadata.VectorBytes = byteops.Float32ToByteVector(vectorfmt)
   231  			}
   232  		}
   233  	}
   234  
   235  	if len(additionalPropsParams.Vectors) > 0 {
   236  		vectors, ok := additionalPropertiesMap["vectors"]
   237  		if ok {
   238  			vectorfmt, ok2 := vectors.(map[string][]float32)
   239  			if ok2 {
   240  				metadata.Vectors = make([]*pb.Vectors, 0, len(vectorfmt))
   241  				for name, vector := range vectorfmt {
   242  					metadata.Vectors = append(metadata.Vectors, &pb.Vectors{
   243  						VectorBytes: byteops.Float32ToByteVector(vector),
   244  						Name:        name,
   245  					})
   246  				}
   247  			}
   248  
   249  		}
   250  	}
   251  
   252  	if additionalPropsParams.Certainty {
   253  		metadata.CertaintyPresent = false
   254  		certainty, ok := additionalPropertiesMap["certainty"]
   255  		if ok {
   256  			certaintyfmt, ok2 := certainty.(float64)
   257  			if ok2 {
   258  				metadata.Certainty = float32(certaintyfmt)
   259  				metadata.CertaintyPresent = true
   260  			}
   261  		}
   262  	}
   263  
   264  	if additionalPropsParams.Distance {
   265  		metadata.DistancePresent = false
   266  		distance, ok := additionalPropertiesMap["distance"]
   267  		if ok {
   268  			distancefmt, ok2 := distance.(float32)
   269  			if ok2 {
   270  				metadata.Distance = distancefmt
   271  				metadata.DistancePresent = true
   272  			}
   273  		}
   274  	}
   275  
   276  	if additionalPropsParams.CreationTimeUnix {
   277  		metadata.CreationTimeUnixPresent = false
   278  		creationtime, ok := additionalPropertiesMap["creationTimeUnix"]
   279  		if ok {
   280  			creationtimefmt, ok2 := creationtime.(int64)
   281  			if ok2 {
   282  				metadata.CreationTimeUnix = creationtimefmt
   283  				metadata.CreationTimeUnixPresent = true
   284  			}
   285  		}
   286  	}
   287  
   288  	if additionalPropsParams.LastUpdateTimeUnix {
   289  		metadata.LastUpdateTimeUnixPresent = false
   290  		lastUpdateTime, ok := additionalPropertiesMap["lastUpdateTimeUnix"]
   291  		if ok {
   292  			lastUpdateTimefmt, ok2 := lastUpdateTime.(int64)
   293  			if ok2 {
   294  				metadata.LastUpdateTimeUnix = lastUpdateTimefmt
   295  				metadata.LastUpdateTimeUnixPresent = true
   296  			}
   297  		}
   298  	}
   299  
   300  	if additionalPropsParams.ExplainScore {
   301  		metadata.ExplainScorePresent = false
   302  		explainScore, ok := additionalPropertiesMap["explainScore"]
   303  		if ok {
   304  			explainScorefmt, ok2 := explainScore.(string)
   305  			if ok2 {
   306  				metadata.ExplainScore = explainScorefmt
   307  				metadata.ExplainScorePresent = true
   308  			}
   309  		}
   310  	}
   311  
   312  	if additionalPropsParams.Score {
   313  		metadata.ScorePresent = false
   314  		score, ok := additionalPropertiesMap["score"]
   315  		if ok {
   316  			scorefmt, ok2 := score.(float32)
   317  			if ok2 {
   318  				metadata.Score = scorefmt
   319  				metadata.ScorePresent = true
   320  			}
   321  		}
   322  	}
   323  
   324  	if additionalPropsParams.IsConsistent {
   325  		isConsistent, ok := additionalPropertiesMap["isConsistent"]
   326  		if ok {
   327  			isConsistentfmt, ok2 := isConsistent.(bool)
   328  			if ok2 {
   329  				metadata.IsConsistent = &isConsistentfmt
   330  				metadata.IsConsistentPresent = true
   331  			}
   332  		}
   333  	}
   334  
   335  	return metadata, generativeGroupResults, nil
   336  }
   337  
   338  func extractGroup(raw any, searchParams dto.GetParams, scheme schema.Schema, usesMarshalling bool) (*pb.GroupByResult, string, error) {
   339  	generativeSearchRaw, generativeSearchEnabled := searchParams.AdditionalProperties.ModuleParams["generate"]
   340  	_, rerankEnabled := searchParams.AdditionalProperties.ModuleParams["rerank"]
   341  	asMap, ok := raw.(map[string]interface{})
   342  	if !ok {
   343  		return nil, "", fmt.Errorf("cannot parse result %v", raw)
   344  	}
   345  	add, ok := asMap["_additional"]
   346  	if !ok {
   347  		return nil, "", fmt.Errorf("_additional is required for groups %v", asMap)
   348  	}
   349  	addAsMap, ok := add.(map[string]interface{})
   350  	if !ok {
   351  		return nil, "", fmt.Errorf("cannot parse _additional %v", add)
   352  	}
   353  	groupRaw, ok := addAsMap["group"]
   354  	if !ok {
   355  		return nil, "", fmt.Errorf("group is not present %v", addAsMap)
   356  	}
   357  	group, ok := groupRaw.(*additional.Group)
   358  	if !ok {
   359  		return nil, "", fmt.Errorf("cannot parse _additional %v", groupRaw)
   360  	}
   361  
   362  	ret := &pb.GroupByResult{
   363  		Name:            group.GroupedBy.Value,
   364  		MaxDistance:     group.MaxDistance,
   365  		MinDistance:     group.MinDistance,
   366  		NumberOfObjects: int64(group.Count),
   367  	}
   368  
   369  	groupedGenerativeResults := ""
   370  	if generativeSearchEnabled {
   371  		var generateFmt *models.GenerateResult
   372  
   373  		generate, ok := addAsMap["generate"]
   374  		if !ok {
   375  			generateFmt = &models.GenerateResult{}
   376  		} else {
   377  			generateFmt, ok = generate.(*models.GenerateResult)
   378  			if !ok {
   379  				return nil, "", errors.New("could not cast generative result additional prop")
   380  			}
   381  		}
   382  
   383  		generativeSearch, ok := generativeSearchRaw.(*generative.Params)
   384  		if !ok {
   385  			return nil, "", errors.New("could not cast generative search params")
   386  		}
   387  		if generativeSearch.Prompt != nil && generateFmt.SingleResult == nil {
   388  			return nil, "", errors.New("No results for generative search despite a search request. Is a generative module enabled?")
   389  		}
   390  
   391  		if generateFmt.Error != nil {
   392  			return nil, "", generateFmt.Error
   393  		}
   394  
   395  		if generateFmt.SingleResult != nil && *generateFmt.SingleResult != "" {
   396  			ret.Generative = &pb.GenerativeReply{Result: *generateFmt.SingleResult}
   397  		}
   398  
   399  		// grouped results are only added to the first object for GQL reasons
   400  		// however, reranking can result in a different order, so we need to check every object
   401  		// recording the result if it's present assuming that it is at least somewhere and will be caught
   402  		if generateFmt.GroupedResult != nil && *generateFmt.GroupedResult != "" {
   403  			groupedGenerativeResults = *generateFmt.GroupedResult
   404  		}
   405  	}
   406  
   407  	if rerankEnabled {
   408  		rerankRaw, ok := addAsMap["rerank"]
   409  		if !ok {
   410  			return nil, "", fmt.Errorf("rerank is not present %v", addAsMap)
   411  		}
   412  		rerank, ok := rerankRaw.([]*models.RankResult)
   413  		if !ok {
   414  			return nil, "", fmt.Errorf("cannot parse rerank %v", rerankRaw)
   415  		}
   416  		ret.Rerank = &pb.RerankReply{
   417  			Score: *rerank[0].Score,
   418  		}
   419  	}
   420  
   421  	// group results does not support more additional properties
   422  	searchParams.AdditionalProperties = additional.Properties{
   423  		ID:       searchParams.AdditionalProperties.ID,
   424  		Vector:   searchParams.AdditionalProperties.Vector,
   425  		Distance: searchParams.AdditionalProperties.Distance,
   426  	}
   427  
   428  	// group objects are returned as a different type than normal results ([]map[string]interface{} vs []interface). As
   429  	// the normal path is used much more often than groupBy, convert the []map[string]interface{} to []interface{}, even
   430  	// though we cast it to map[string]interface{} in the extraction function.
   431  	// This way we only do a copy for groupBy and not for the standard code-path which is used more often
   432  	returnObjectsUntyped := make([]interface{}, len(group.Hits))
   433  	for i := range returnObjectsUntyped {
   434  		returnObjectsUntyped[i] = group.Hits[i]
   435  	}
   436  
   437  	objects, _, err := extractObjectsToResults(returnObjectsUntyped, searchParams, scheme, true, usesMarshalling)
   438  	if err != nil {
   439  		return nil, "", errors.Wrap(err, "extracting hits from group")
   440  	}
   441  
   442  	ret.Objects = objects
   443  
   444  	return ret, groupedGenerativeResults, nil
   445  }
   446  
   447  func extractPropertiesAnswerDeprecated(scheme schema.Schema, results map[string]interface{}, properties search.SelectProperties, className string, additionalPropsParams additional.Properties) (*pb.PropertiesResult, error) {
   448  	nonRefProps := make(map[string]interface{}, 0)
   449  	refProps := make([]*pb.RefPropertiesResult, 0)
   450  	objProps := make([]*pb.ObjectProperties, 0)
   451  	objArrayProps := make([]*pb.ObjectArrayProperties, 0)
   452  	for _, prop := range properties {
   453  		propRaw, ok := results[prop.Name]
   454  		if !ok {
   455  			continue
   456  		}
   457  		if prop.IsPrimitive {
   458  			nonRefProps[prop.Name] = propRaw
   459  			continue
   460  		}
   461  		if prop.IsObject {
   462  			nested, err := scheme.GetProperty(schema.ClassName(className), schema.PropertyName(prop.Name))
   463  			if err != nil {
   464  				return nil, errors.Wrap(err, "getting property")
   465  			}
   466  			singleObj, ok := propRaw.(map[string]interface{})
   467  			if ok {
   468  				extractedNestedProp, err := extractPropertiesNested(scheme, singleObj, prop, className, &Property{Property: nested})
   469  				if err != nil {
   470  					return nil, errors.Wrap(err, "extracting nested properties")
   471  				}
   472  				objProps = append(objProps, &pb.ObjectProperties{
   473  					PropName: prop.Name,
   474  					Value:    extractedNestedProp,
   475  				})
   476  				continue
   477  			}
   478  			arrayObjs, ok := propRaw.([]interface{})
   479  			if ok {
   480  				extractedNestedProps := make([]*pb.ObjectPropertiesValue, 0, len(arrayObjs))
   481  				for _, obj := range arrayObjs {
   482  					singleObj, ok := obj.(map[string]interface{})
   483  					if !ok {
   484  						continue
   485  					}
   486  					extractedNestedProp, err := extractPropertiesNested(scheme, singleObj, prop, className, &Property{Property: nested})
   487  					if err != nil {
   488  						return nil, err
   489  					}
   490  					extractedNestedProps = append(extractedNestedProps, extractedNestedProp)
   491  				}
   492  				objArrayProps = append(objArrayProps,
   493  					&pb.ObjectArrayProperties{
   494  						PropName: prop.Name,
   495  						Values:   extractedNestedProps,
   496  					},
   497  				)
   498  				continue
   499  			}
   500  		}
   501  		refs, ok := propRaw.([]interface{})
   502  		if !ok {
   503  			continue
   504  		}
   505  		extractedRefProps := make([]*pb.PropertiesResult, 0, len(refs))
   506  		for _, ref := range refs {
   507  			refLocal, ok := ref.(search.LocalRef)
   508  			if !ok {
   509  				continue
   510  			}
   511  			extractedRefProp, err := extractPropertiesAnswerDeprecated(scheme, refLocal.Fields, prop.Refs[0].RefProperties, refLocal.Class, additionalPropsParams)
   512  			if err != nil {
   513  				continue
   514  			}
   515  			additionalProps, _, err := extractAdditionalProps(refLocal.Fields, prop.Refs[0].AdditionalProperties, false, false)
   516  			if err != nil {
   517  				return nil, err
   518  			}
   519  			extractedRefProp.Metadata = additionalProps
   520  			extractedRefProps = append(extractedRefProps, extractedRefProp)
   521  		}
   522  
   523  		refProp := pb.RefPropertiesResult{PropName: prop.Name, Properties: extractedRefProps}
   524  		refProps = append(refProps, &refProp)
   525  	}
   526  	props := pb.PropertiesResult{}
   527  	if len(nonRefProps) > 0 {
   528  		outProps := pb.ObjectPropertiesValue{}
   529  		if err := extractArrayTypesRoot(scheme, className, nonRefProps, &outProps); err != nil {
   530  			return nil, errors.Wrap(err, "extracting non-primitive types")
   531  		}
   532  		newStruct, err := structpb.NewStruct(nonRefProps)
   533  		if err != nil {
   534  			return nil, errors.Wrap(err, "creating non-ref-prop struct")
   535  		}
   536  		props.NonRefProperties = newStruct
   537  		props.IntArrayProperties = outProps.IntArrayProperties
   538  		props.NumberArrayProperties = outProps.NumberArrayProperties
   539  		props.TextArrayProperties = outProps.TextArrayProperties
   540  		props.BooleanArrayProperties = outProps.BooleanArrayProperties
   541  		props.ObjectProperties = outProps.ObjectProperties
   542  		props.ObjectArrayProperties = outProps.ObjectArrayProperties
   543  	}
   544  	if len(refProps) > 0 {
   545  		props.RefProps = refProps
   546  	}
   547  	if len(objProps) > 0 {
   548  		props.ObjectProperties = objProps
   549  	}
   550  	if len(objArrayProps) > 0 {
   551  		props.ObjectArrayProperties = objArrayProps
   552  	}
   553  
   554  	props.TargetCollection = className
   555  	return &props, nil
   556  }
   557  
   558  func extractPropertiesAnswer(scheme schema.Schema, results map[string]interface{}, properties search.SelectProperties, className string, additionalPropsParams additional.Properties) (*pb.PropertiesResult, error) {
   559  	nonRefProps := &pb.Properties{
   560  		Fields: make(map[string]*pb.Value, 0),
   561  	}
   562  	refProps := make([]*pb.RefPropertiesResult, 0)
   563  	class := scheme.GetClass(schema.ClassName(className))
   564  	for _, prop := range properties {
   565  		propRaw, ok := results[prop.Name]
   566  
   567  		if !ok {
   568  			if prop.IsPrimitive || prop.IsObject {
   569  				nonRefProps.Fields[prop.Name] = NewNilValue()
   570  			}
   571  			continue
   572  		}
   573  		if prop.IsPrimitive {
   574  			dataType, err := schema.GetPropertyDataType(class, prop.Name)
   575  			if err != nil {
   576  				return nil, errors.Wrap(err, "getting primitive property datatype")
   577  			}
   578  			value, err := NewPrimitiveValue(propRaw, *dataType)
   579  			if err != nil {
   580  				return nil, errors.Wrapf(err, "creating primitive value for %v", prop.Name)
   581  			}
   582  			nonRefProps.Fields[prop.Name] = value
   583  			continue
   584  		}
   585  		if prop.IsObject {
   586  			nested, err := scheme.GetProperty(schema.ClassName(className), schema.PropertyName(prop.Name))
   587  			if err != nil {
   588  				return nil, errors.Wrap(err, "getting nested property")
   589  			}
   590  			value, err := NewNestedValue(propRaw, schema.DataType(nested.DataType[0]), &Property{Property: nested}, prop)
   591  			if err != nil {
   592  				return nil, errors.Wrap(err, "creating object value")
   593  			}
   594  			nonRefProps.Fields[prop.Name] = value
   595  			continue
   596  		}
   597  		refs, ok := propRaw.([]interface{})
   598  		if !ok {
   599  			continue
   600  		}
   601  		extractedRefProps := make([]*pb.PropertiesResult, 0, len(refs))
   602  		for _, ref := range refs {
   603  			refLocal, ok := ref.(search.LocalRef)
   604  			if !ok {
   605  				continue
   606  			}
   607  			extractedRefProp, err := extractPropertiesAnswer(scheme, refLocal.Fields, prop.Refs[0].RefProperties, refLocal.Class, additionalPropsParams)
   608  			if err != nil {
   609  				continue
   610  			}
   611  			additionalProps, _, err := extractAdditionalProps(refLocal.Fields, prop.Refs[0].AdditionalProperties, false, false)
   612  			if err != nil {
   613  				return nil, err
   614  			}
   615  			extractedRefProp.Metadata = additionalProps
   616  			extractedRefProps = append(extractedRefProps, extractedRefProp)
   617  		}
   618  
   619  		refProp := pb.RefPropertiesResult{PropName: prop.Name, Properties: extractedRefProps}
   620  		refProps = append(refProps, &refProp)
   621  	}
   622  	props := pb.PropertiesResult{}
   623  	if len(nonRefProps.Fields) != 0 {
   624  		props.NonRefProps = nonRefProps
   625  	}
   626  	if len(refProps) != 0 {
   627  		props.RefProps = refProps
   628  	}
   629  	props.RefPropsRequested = properties.HasRefs()
   630  	props.TargetCollection = className
   631  	return &props, nil
   632  }
   633  
   634  func extractPropertiesNested[P schema.PropertyInterface](scheme schema.Schema, results map[string]interface{}, property search.SelectProperty, className string, parent P) (*pb.ObjectPropertiesValue, error) {
   635  	primitiveProps := make(map[string]interface{}, 0)
   636  	objProps := make([]*pb.ObjectProperties, 0)
   637  	objArrayProps := make([]*pb.ObjectArrayProperties, 0)
   638  	for _, prop := range property.Props {
   639  		propRaw, ok := results[prop.Name]
   640  		if !ok {
   641  			continue
   642  		}
   643  		if prop.IsPrimitive {
   644  			primitiveProps[prop.Name] = propRaw
   645  			continue
   646  		}
   647  		if prop.IsObject {
   648  			var err error
   649  			objProps, objArrayProps, err = extractObjectProperties(scheme, propRaw, prop, className, parent, objProps, objArrayProps)
   650  			if err != nil {
   651  				return nil, err
   652  			}
   653  		}
   654  	}
   655  	props := pb.ObjectPropertiesValue{}
   656  	if len(primitiveProps) > 0 {
   657  		if err := extractArrayTypesNested(scheme, className, primitiveProps, &props, parent); err != nil {
   658  			return nil, errors.Wrap(err, "extracting non-primitive types")
   659  		}
   660  		newStruct, err := structpb.NewStruct(primitiveProps)
   661  		if err != nil {
   662  			return nil, errors.Wrap(err, "creating non-ref-prop struct")
   663  		}
   664  		props.NonRefProperties = newStruct
   665  	}
   666  	if len(objProps) > 0 {
   667  		props.ObjectProperties = objProps
   668  	}
   669  	if len(objArrayProps) > 0 {
   670  		props.ObjectArrayProperties = objArrayProps
   671  	}
   672  	return &props, nil
   673  }
   674  
   675  func extractObjectProperties[P schema.PropertyInterface](scheme schema.Schema, propRaw interface{}, property search.SelectProperty, className string, parent P, objProps []*pb.ObjectProperties, objArrayProps []*pb.ObjectArrayProperties) ([]*pb.ObjectProperties, []*pb.ObjectArrayProperties, error) {
   676  	prop, ok := propRaw.(map[string]interface{})
   677  	if ok {
   678  		objProp, err := extractObjectSingleProperties(scheme, prop, property, className, parent)
   679  		if err != nil {
   680  			return objProps, objArrayProps, err
   681  		}
   682  		objProps = append(objProps, objProp)
   683  	}
   684  	propArray, ok := propRaw.([]interface{})
   685  	if ok {
   686  		objArrayProp, err := extractObjectArrayProperties(scheme, propArray, property, className, parent)
   687  		if err != nil {
   688  			return objProps, objArrayProps, err
   689  		}
   690  		objArrayProps = append(objArrayProps, objArrayProp)
   691  	}
   692  	return objProps, objArrayProps, nil
   693  }
   694  
   695  func extractObjectSingleProperties[P schema.PropertyInterface](scheme schema.Schema, prop map[string]interface{}, property search.SelectProperty, className string, parent P) (*pb.ObjectProperties, error) {
   696  	nested, err := schema.GetNestedPropertyByName(parent, property.Name)
   697  	if err != nil {
   698  		return nil, errors.Wrap(err, "getting property")
   699  	}
   700  	extractedNestedProp, err := extractPropertiesNested(scheme, prop, property, className, &NestedProperty{NestedProperty: nested})
   701  	if err != nil {
   702  		return nil, errors.Wrap(err, fmt.Sprintf("extracting nested properties from %v", nested))
   703  	}
   704  	return &pb.ObjectProperties{
   705  		PropName: property.Name,
   706  		Value:    extractedNestedProp,
   707  	}, nil
   708  }
   709  
   710  func extractObjectArrayProperties[P schema.PropertyInterface](scheme schema.Schema, propObjs []interface{}, property search.SelectProperty, className string, parent P) (*pb.ObjectArrayProperties, error) {
   711  	extractedNestedProps := make([]*pb.ObjectPropertiesValue, 0, len(propObjs))
   712  	for _, objRaw := range propObjs {
   713  		nested, err := schema.GetNestedPropertyByName(parent, property.Name)
   714  		if err != nil {
   715  			return nil, errors.Wrap(err, "getting property")
   716  		}
   717  		obj, ok := objRaw.(map[string]interface{})
   718  		if !ok {
   719  			continue
   720  		}
   721  		extractedNestedProp, err := extractPropertiesNested(scheme, obj, property, className, &NestedProperty{NestedProperty: nested})
   722  		if err != nil {
   723  			return nil, errors.Wrap(err, "extracting nested properties")
   724  		}
   725  		extractedNestedProps = append(extractedNestedProps, extractedNestedProp)
   726  	}
   727  	return &pb.ObjectArrayProperties{
   728  		PropName: property.Name,
   729  		Values:   extractedNestedProps,
   730  	}, nil
   731  }
   732  
   733  func extractArrayTypesRoot(scheme schema.Schema, className string, rawProps map[string]interface{}, props *pb.ObjectPropertiesValue) error {
   734  	dataTypes := make(map[string]*schema.DataType, 0)
   735  	for propName := range rawProps {
   736  		dataType, err := schema.GetPropertyDataType(scheme.GetClass(schema.ClassName(className)), propName)
   737  		if err != nil {
   738  			return err
   739  		}
   740  		dataTypes[propName] = dataType
   741  	}
   742  	return extractArrayTypes(scheme, rawProps, props, dataTypes)
   743  }
   744  
   745  func extractArrayTypesNested[P schema.PropertyInterface](scheme schema.Schema, className string, rawProps map[string]interface{}, props *pb.ObjectPropertiesValue, parent P) error {
   746  	dataTypes := make(map[string]*schema.DataType, 0)
   747  	for propName := range rawProps {
   748  		dataType, err := schema.GetNestedPropertyDataType(parent, propName)
   749  		if err != nil {
   750  			return err
   751  		}
   752  		dataTypes[propName] = dataType
   753  	}
   754  	return extractArrayTypes(scheme, rawProps, props, dataTypes)
   755  }
   756  
   757  // slices cannot be part of a grpc struct, so we need to handle each of them separately
   758  func extractArrayTypes(scheme schema.Schema, rawProps map[string]interface{}, props *pb.ObjectPropertiesValue, dataTypes map[string]*schema.DataType) error {
   759  	for propName, prop := range rawProps {
   760  		dataType := dataTypes[propName]
   761  		switch *dataType {
   762  		case schema.DataTypeIntArray:
   763  			propIntAsFloat, ok := prop.([]float64)
   764  			if !ok {
   765  				emptyArr, ok := prop.([]interface{})
   766  				if ok && len(emptyArr) == 0 {
   767  					continue
   768  				}
   769  				return fmt.Errorf("property %v with datatype %v needs to be []float64, got %T", propName, dataType, prop)
   770  			}
   771  			propInt := make([]int64, len(propIntAsFloat))
   772  			for i := range propIntAsFloat {
   773  				propInt[i] = int64(propIntAsFloat[i])
   774  			}
   775  			if props.IntArrayProperties == nil {
   776  				props.IntArrayProperties = make([]*pb.IntArrayProperties, 0)
   777  			}
   778  			props.IntArrayProperties = append(props.IntArrayProperties, &pb.IntArrayProperties{PropName: propName, Values: propInt})
   779  			delete(rawProps, propName)
   780  		case schema.DataTypeNumberArray:
   781  			propFloat, ok := prop.([]float64)
   782  			if !ok {
   783  				emptyArr, ok := prop.([]interface{})
   784  				if ok && len(emptyArr) == 0 {
   785  					continue
   786  				}
   787  				return fmt.Errorf("property %v with datatype %v needs to be []float64, got %T", propName, dataType, prop)
   788  			}
   789  
   790  			if props.NumberArrayProperties == nil {
   791  				props.NumberArrayProperties = make([]*pb.NumberArrayProperties, 0)
   792  			}
   793  			props.NumberArrayProperties = append(
   794  				props.NumberArrayProperties,
   795  				&pb.NumberArrayProperties{PropName: propName, ValuesBytes: byteops.Float64ToByteVector(propFloat), Values: propFloat},
   796  			)
   797  			delete(rawProps, propName)
   798  		case schema.DataTypeStringArray, schema.DataTypeTextArray, schema.DataTypeDateArray, schema.DataTypeUUIDArray:
   799  			propString, ok := prop.([]string)
   800  			if !ok {
   801  				emptyArr, ok := prop.([]interface{})
   802  				if ok && len(emptyArr) == 0 {
   803  					continue
   804  				}
   805  				return fmt.Errorf("property %v with datatype %v needs to be []string, got %T", propName, dataType, prop)
   806  			}
   807  			if props.TextArrayProperties == nil {
   808  				props.TextArrayProperties = make([]*pb.TextArrayProperties, 0)
   809  			}
   810  			props.TextArrayProperties = append(props.TextArrayProperties, &pb.TextArrayProperties{PropName: propName, Values: propString})
   811  			delete(rawProps, propName)
   812  		case schema.DataTypeBooleanArray:
   813  			propBool, ok := prop.([]bool)
   814  			if !ok {
   815  				emptyArr, ok := prop.([]interface{})
   816  				if ok && len(emptyArr) == 0 {
   817  					continue
   818  				}
   819  				return fmt.Errorf("property %v with datatype %v needs to be []bool, got %T", propName, dataType, prop)
   820  			}
   821  			if props.BooleanArrayProperties == nil {
   822  				props.BooleanArrayProperties = make([]*pb.BooleanArrayProperties, 0)
   823  			}
   824  			props.BooleanArrayProperties = append(props.BooleanArrayProperties, &pb.BooleanArrayProperties{PropName: propName, Values: propBool})
   825  			delete(rawProps, propName)
   826  		default:
   827  			_, isArray := schema.IsArrayType(*dataType)
   828  			if isArray {
   829  				return fmt.Errorf("property %v with array type not handled %v", propName, dataType)
   830  			}
   831  		}
   832  	}
   833  	return nil
   834  }