github.com/weaviate/weaviate@v1.24.6/adapters/repos/db/refcache/resolver_test.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 refcache
    13  
    14  import (
    15  	"context"
    16  	"fmt"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/go-openapi/strfmt"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  	"github.com/weaviate/weaviate/entities/additional"
    24  	"github.com/weaviate/weaviate/entities/models"
    25  	"github.com/weaviate/weaviate/entities/multi"
    26  	"github.com/weaviate/weaviate/entities/search"
    27  )
    28  
    29  func TestResolver(t *testing.T) {
    30  	id1 := "df5d4e49-0c56-4b87-ade1-3d46cc9b425f"
    31  	id2 := "3a08d808-8eb5-49ee-86b2-68b6035e8b69"
    32  
    33  	t.Run("with nil input", func(t *testing.T) {
    34  		r := NewResolver(newFakeCacher())
    35  		res, err := r.Do(context.Background(), nil, nil, additional.Properties{})
    36  		require.Nil(t, err)
    37  		assert.Nil(t, res)
    38  	})
    39  
    40  	t.Run("with nil-schemas", func(t *testing.T) {
    41  		r := NewResolver(newFakeCacher())
    42  		input := []search.Result{
    43  			{
    44  				ID:        "foo",
    45  				ClassName: "BestClass",
    46  			},
    47  		}
    48  
    49  		expected := input
    50  		res, err := r.Do(context.Background(), input, nil, additional.Properties{})
    51  		require.Nil(t, err)
    52  		assert.Equal(t, expected, res)
    53  	})
    54  
    55  	t.Run("with single ref but no select props", func(t *testing.T) {
    56  		r := NewResolver(newFakeCacher())
    57  		input := []search.Result{
    58  			{
    59  				ID:        "foo",
    60  				ClassName: "BestClass",
    61  				Schema: map[string]interface{}{
    62  					"refProp": models.MultipleRef{
    63  						&models.SingleRef{
    64  							Beacon: "weaviate://localhost/123",
    65  						},
    66  					},
    67  				},
    68  			},
    69  		}
    70  
    71  		expected := input
    72  		res, err := r.Do(context.Background(), input, nil, additional.Properties{})
    73  		require.Nil(t, err)
    74  		assert.Equal(t, expected, res)
    75  	})
    76  
    77  	t.Run("with single ref with vector and matching select prop", func(t *testing.T) {
    78  		getInput := func() []search.Result {
    79  			return []search.Result{
    80  				{
    81  					ID:        "foo",
    82  					ClassName: "BestClass",
    83  					Schema: map[string]interface{}{
    84  						"refProp": models.MultipleRef{
    85  							&models.SingleRef{
    86  								Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", id1)),
    87  							},
    88  						},
    89  					},
    90  				},
    91  			}
    92  		}
    93  		getResolver := func() *Resolver {
    94  			cacher := newFakeCacher()
    95  			r := NewResolver(cacher)
    96  			cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
    97  				ClassName: "SomeClass",
    98  				ID:        strfmt.UUID(id1),
    99  				Schema: map[string]interface{}{
   100  					"bar": "some string",
   101  				},
   102  				Vector: []float32{0.1, 0.2},
   103  			}
   104  			return r
   105  		}
   106  		getSelectProps := func(withVector bool) search.SelectProperties {
   107  			return search.SelectProperties{
   108  				search.SelectProperty{
   109  					Name: "refProp",
   110  					Refs: []search.SelectClass{
   111  						{
   112  							ClassName: "SomeClass",
   113  							RefProperties: search.SelectProperties{
   114  								search.SelectProperty{
   115  									Name:        "bar",
   116  									IsPrimitive: true,
   117  								},
   118  							},
   119  							AdditionalProperties: additional.Properties{
   120  								Vector: withVector,
   121  							},
   122  						},
   123  					},
   124  				},
   125  			}
   126  		}
   127  		getExpectedResult := func(withVector bool) []search.Result {
   128  			fields := map[string]interface{}{
   129  				"bar": "some string",
   130  			}
   131  			if withVector {
   132  				fields["vector"] = []float32{0.1, 0.2}
   133  			}
   134  			return []search.Result{
   135  				{
   136  					ID:        "foo",
   137  					ClassName: "BestClass",
   138  					Schema: map[string]interface{}{
   139  						"refProp": []interface{}{
   140  							search.LocalRef{
   141  								Class:  "SomeClass",
   142  								Fields: fields,
   143  							},
   144  						},
   145  					},
   146  				},
   147  			}
   148  		}
   149  		// ask for vector in ref property
   150  		res, err := getResolver().Do(context.Background(), getInput(), getSelectProps(true), additional.Properties{})
   151  		require.Nil(t, err)
   152  		assert.Equal(t, getExpectedResult(true), res)
   153  		// don't ask for vector in ref property
   154  		res, err = getResolver().Do(context.Background(), getInput(), getSelectProps(false), additional.Properties{})
   155  		require.Nil(t, err)
   156  		assert.Equal(t, getExpectedResult(false), res)
   157  	})
   158  
   159  	t.Run("with single ref with creation/update timestamps and matching select prop", func(t *testing.T) {
   160  		now := time.Now().UnixMilli()
   161  		getInput := func() []search.Result {
   162  			return []search.Result{
   163  				{
   164  					ID:        "foo",
   165  					ClassName: "BestClass",
   166  					Schema: map[string]interface{}{
   167  						"refProp": models.MultipleRef{
   168  							&models.SingleRef{
   169  								Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", id1)),
   170  							},
   171  						},
   172  					},
   173  				},
   174  			}
   175  		}
   176  		getResolver := func() *Resolver {
   177  			cacher := newFakeCacher()
   178  			r := NewResolver(cacher)
   179  			cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
   180  				ClassName: "SomeClass",
   181  				ID:        strfmt.UUID(id1),
   182  				Schema: map[string]interface{}{
   183  					"bar": "some string",
   184  				},
   185  				Created: now,
   186  				Updated: now,
   187  			}
   188  			return r
   189  		}
   190  		selectProps := search.SelectProperties{
   191  			search.SelectProperty{
   192  				Name: "refProp",
   193  				Refs: []search.SelectClass{
   194  					{
   195  						ClassName: "SomeClass",
   196  						RefProperties: search.SelectProperties{
   197  							search.SelectProperty{
   198  								Name:        "bar",
   199  								IsPrimitive: true,
   200  							},
   201  						},
   202  						AdditionalProperties: additional.Properties{
   203  							CreationTimeUnix:   true,
   204  							LastUpdateTimeUnix: true,
   205  						},
   206  					},
   207  				},
   208  			},
   209  		}
   210  		expected := []search.Result{
   211  			{
   212  				ID:        "foo",
   213  				ClassName: "BestClass",
   214  				Schema: map[string]interface{}{
   215  					"refProp": []interface{}{
   216  						search.LocalRef{
   217  							Class: "SomeClass",
   218  							Fields: map[string]interface{}{
   219  								"bar":                "some string",
   220  								"creationTimeUnix":   now,
   221  								"lastUpdateTimeUnix": now,
   222  							},
   223  						},
   224  					},
   225  				},
   226  			},
   227  		}
   228  		res, err := getResolver().Do(context.Background(), getInput(), selectProps, additional.Properties{})
   229  		require.Nil(t, err)
   230  		assert.Equal(t, expected, res)
   231  	})
   232  
   233  	t.Run("with single ref and matching select prop", func(t *testing.T) {
   234  		cacher := newFakeCacher()
   235  		r := NewResolver(cacher)
   236  		cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
   237  			ClassName: "SomeClass",
   238  			ID:        strfmt.UUID(id1),
   239  			Schema: map[string]interface{}{
   240  				"bar": "some string",
   241  			},
   242  		}
   243  		input := []search.Result{
   244  			{
   245  				ID:        "foo",
   246  				ClassName: "BestClass",
   247  				Schema: map[string]interface{}{
   248  					"refProp": models.MultipleRef{
   249  						&models.SingleRef{
   250  							Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", id1)),
   251  						},
   252  					},
   253  				},
   254  			},
   255  		}
   256  		selectProps := search.SelectProperties{
   257  			search.SelectProperty{
   258  				Name: "refProp",
   259  				Refs: []search.SelectClass{
   260  					{
   261  						ClassName: "SomeClass",
   262  						RefProperties: search.SelectProperties{
   263  							search.SelectProperty{
   264  								Name:        "bar",
   265  								IsPrimitive: true,
   266  							},
   267  						},
   268  					},
   269  				},
   270  			},
   271  		}
   272  
   273  		expected := []search.Result{
   274  			{
   275  				ID:        "foo",
   276  				ClassName: "BestClass",
   277  				Schema: map[string]interface{}{
   278  					"refProp": []interface{}{
   279  						search.LocalRef{
   280  							Class: "SomeClass",
   281  							Fields: map[string]interface{}{
   282  								"bar": "some string",
   283  							},
   284  						},
   285  					},
   286  				},
   287  			},
   288  		}
   289  		res, err := r.Do(context.Background(), input, selectProps, additional.Properties{})
   290  		require.Nil(t, err)
   291  		assert.Equal(t, expected, res)
   292  	})
   293  
   294  	t.Run("with a nested lookup", func(t *testing.T) {
   295  		cacher := newFakeCacher()
   296  		r := NewResolver(cacher)
   297  		cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
   298  			ClassName: "SomeClass",
   299  			ID:        strfmt.UUID(id1),
   300  			Schema: map[string]interface{}{
   301  				"primitive": "foobar",
   302  				"ignoredRef": models.MultipleRef{
   303  					&models.SingleRef{
   304  						Beacon: strfmt.URI("weaviate://localhost/ignoreMe"),
   305  					},
   306  				},
   307  				"nestedRef": models.MultipleRef{
   308  					&models.SingleRef{
   309  						Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", id2)),
   310  					},
   311  				},
   312  			},
   313  		}
   314  		cacher.lookup[multi.Identifier{ID: id2, ClassName: "SomeNestedClass"}] = search.Result{
   315  			ClassName: "SomeNestedClass",
   316  			ID:        strfmt.UUID(id2),
   317  			Schema: map[string]interface{}{
   318  				"name": "John Doe",
   319  			},
   320  		}
   321  		input := []search.Result{
   322  			{
   323  				ID:        "foo",
   324  				ClassName: "BestClass",
   325  				Schema: map[string]interface{}{
   326  					"refProp": models.MultipleRef{
   327  						&models.SingleRef{
   328  							Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", id1)),
   329  						},
   330  					},
   331  				},
   332  			},
   333  		}
   334  		selectProps := search.SelectProperties{
   335  			search.SelectProperty{
   336  				Name: "refProp",
   337  				Refs: []search.SelectClass{
   338  					{
   339  						ClassName: "SomeClass",
   340  						RefProperties: search.SelectProperties{
   341  							search.SelectProperty{
   342  								Name:        "primitive",
   343  								IsPrimitive: true,
   344  							},
   345  							search.SelectProperty{
   346  								Name: "nestedRef",
   347  								Refs: []search.SelectClass{
   348  									{
   349  										ClassName: "SomeNestedClass",
   350  										RefProperties: []search.SelectProperty{
   351  											{
   352  												Name:        "name",
   353  												IsPrimitive: true,
   354  											},
   355  										},
   356  									},
   357  								},
   358  							},
   359  						},
   360  					},
   361  				},
   362  			},
   363  		}
   364  
   365  		expected := []search.Result{
   366  			{
   367  				ID:        "foo",
   368  				ClassName: "BestClass",
   369  				Schema: map[string]interface{}{
   370  					"refProp": []interface{}{
   371  						search.LocalRef{
   372  							Class: "SomeClass",
   373  							Fields: map[string]interface{}{
   374  								"primitive": "foobar",
   375  								"ignoredRef": models.MultipleRef{
   376  									&models.SingleRef{
   377  										Beacon: strfmt.URI("weaviate://localhost/ignoreMe"),
   378  									},
   379  								},
   380  								"nestedRef": []interface{}{
   381  									search.LocalRef{
   382  										Class: "SomeNestedClass",
   383  										Fields: map[string]interface{}{
   384  											"name": "John Doe",
   385  										},
   386  									},
   387  								},
   388  							},
   389  						},
   390  					},
   391  				},
   392  			},
   393  		}
   394  		res, err := r.Do(context.Background(), input, selectProps, additional.Properties{})
   395  		require.Nil(t, err)
   396  		assert.Equal(t, expected, res)
   397  	})
   398  
   399  	t.Run("with single ref with vector and matching select prop and group", func(t *testing.T) {
   400  		getInput := func() []search.Result {
   401  			return []search.Result{
   402  				{
   403  					ID:        "foo",
   404  					ClassName: "BestClass",
   405  					Schema: map[string]interface{}{
   406  						"refProp": models.MultipleRef{
   407  							&models.SingleRef{
   408  								Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/%s", id1)),
   409  							},
   410  						},
   411  					},
   412  					AdditionalProperties: models.AdditionalProperties{
   413  						"group": &additional.Group{
   414  							Hits: []map[string]interface{}{
   415  								{
   416  									"nestedRef": models.MultipleRef{
   417  										&models.SingleRef{
   418  											Beacon: strfmt.URI(fmt.Sprintf("weaviate://localhost/SomeNestedClass/%s", id2)),
   419  										},
   420  									},
   421  								},
   422  							},
   423  						},
   424  					},
   425  				},
   426  			}
   427  		}
   428  		getResolver := func() *Resolver {
   429  			cacher := newFakeCacher()
   430  			r := NewResolverWithGroup(cacher)
   431  			cacher.lookup[multi.Identifier{ID: id1, ClassName: "SomeClass"}] = search.Result{
   432  				ClassName: "SomeClass",
   433  				ID:        strfmt.UUID(id1),
   434  				Schema: map[string]interface{}{
   435  					"bar": "some string",
   436  				},
   437  				Vector: []float32{0.1, 0.2},
   438  			}
   439  			cacher.lookup[multi.Identifier{ID: id2, ClassName: "SomeNestedClass"}] = search.Result{
   440  				ClassName: "SomeNestedClass",
   441  				ID:        strfmt.UUID(id2),
   442  				Schema: map[string]interface{}{
   443  					"name": "John Doe",
   444  				},
   445  			}
   446  			return r
   447  		}
   448  		getSelectProps := func(withVector bool) search.SelectProperties {
   449  			return search.SelectProperties{
   450  				search.SelectProperty{
   451  					Name: "refProp",
   452  					Refs: []search.SelectClass{
   453  						{
   454  							ClassName: "SomeClass",
   455  							RefProperties: search.SelectProperties{
   456  								search.SelectProperty{
   457  									Name:        "bar",
   458  									IsPrimitive: true,
   459  								},
   460  							},
   461  							AdditionalProperties: additional.Properties{
   462  								Vector: withVector,
   463  							},
   464  						},
   465  					},
   466  				},
   467  				search.SelectProperty{
   468  					Name: "_additional:group:hits:nestedRef",
   469  					Refs: []search.SelectClass{
   470  						{
   471  							ClassName: "SomeNestedClass",
   472  							RefProperties: []search.SelectProperty{
   473  								{
   474  									Name:        "name",
   475  									IsPrimitive: true,
   476  								},
   477  							},
   478  						},
   479  					},
   480  				},
   481  			}
   482  		}
   483  		getExpectedResult := func(withVector bool) []search.Result {
   484  			fields := map[string]interface{}{
   485  				"bar": "some string",
   486  			}
   487  			if withVector {
   488  				fields["vector"] = []float32{0.1, 0.2}
   489  			}
   490  			return []search.Result{
   491  				{
   492  					ID:        "foo",
   493  					ClassName: "BestClass",
   494  					Schema: map[string]interface{}{
   495  						"refProp": []interface{}{
   496  							search.LocalRef{
   497  								Class:  "SomeClass",
   498  								Fields: fields,
   499  							},
   500  						},
   501  					},
   502  					AdditionalProperties: models.AdditionalProperties{
   503  						"group": &additional.Group{
   504  							Hits: []map[string]interface{}{
   505  								{
   506  									"nestedRef": []interface{}{
   507  										search.LocalRef{
   508  											Class: "SomeNestedClass",
   509  											Fields: map[string]interface{}{
   510  												"name": "John Doe",
   511  											},
   512  										},
   513  									},
   514  								},
   515  							},
   516  						},
   517  					},
   518  				},
   519  			}
   520  		}
   521  		// ask for vector in ref property
   522  		res, err := getResolver().Do(context.Background(), getInput(), getSelectProps(true), additional.Properties{})
   523  		require.Nil(t, err)
   524  		assert.Equal(t, getExpectedResult(true), res)
   525  		// don't ask for vector in ref property
   526  		res, err = getResolver().Do(context.Background(), getInput(), getSelectProps(false), additional.Properties{})
   527  		require.Nil(t, err)
   528  		assert.Equal(t, getExpectedResult(false), res)
   529  	})
   530  }
   531  
   532  func newFakeCacher() *fakeCacher {
   533  	return &fakeCacher{
   534  		lookup: map[multi.Identifier]search.Result{},
   535  	}
   536  }
   537  
   538  type fakeCacher struct {
   539  	lookup map[multi.Identifier]search.Result
   540  }
   541  
   542  func (f *fakeCacher) Build(ctx context.Context, objects []search.Result, properties search.SelectProperties,
   543  	additional additional.Properties,
   544  ) error {
   545  	return nil
   546  }
   547  
   548  func (f *fakeCacher) Get(si multi.Identifier) (search.Result, bool) {
   549  	res, ok := f.lookup[si]
   550  	return res, ok
   551  }