github.com/weaviate/weaviate@v1.24.6/usecases/classification/classifier_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 classification
    13  
    14  import (
    15  	"context"
    16  	"encoding/json"
    17  	"errors"
    18  	"fmt"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/go-openapi/strfmt"
    23  	"github.com/sirupsen/logrus"
    24  	"github.com/sirupsen/logrus/hooks/test"
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  	"github.com/weaviate/weaviate/entities/models"
    28  	testhelper "github.com/weaviate/weaviate/test/helper"
    29  )
    30  
    31  func newNullLogger() *logrus.Logger {
    32  	log, _ := test.NewNullLogger()
    33  	return log
    34  }
    35  
    36  func Test_Classifier_KNN(t *testing.T) {
    37  	t.Run("with invalid data", func(t *testing.T) {
    38  		sg := &fakeSchemaGetter{testSchema()}
    39  		_, err := New(sg, nil, nil, &fakeAuthorizer{}, newNullLogger(), nil).
    40  			Schedule(context.Background(), nil, models.Classification{})
    41  		assert.NotNil(t, err, "should error with invalid user input")
    42  	})
    43  
    44  	var id strfmt.UUID
    45  	// so we can reuse it for follow up requests, such as checking the status
    46  
    47  	t.Run("with valid data", func(t *testing.T) {
    48  		sg := &fakeSchemaGetter{testSchema()}
    49  		repo := newFakeClassificationRepo()
    50  		authorizer := &fakeAuthorizer{}
    51  		vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified())
    52  		classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil)
    53  
    54  		params := models.Classification{
    55  			Class:              "Article",
    56  			BasedOnProperties:  []string{"description"},
    57  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
    58  			Settings: map[string]interface{}{
    59  				"k": json.Number("1"),
    60  			},
    61  		}
    62  
    63  		t.Run("scheduling a classification", func(t *testing.T) {
    64  			class, err := classifier.Schedule(context.Background(), nil, params)
    65  			require.Nil(t, err, "should not error")
    66  			require.NotNil(t, class)
    67  
    68  			assert.Len(t, class.ID, 36, "an id was assigned")
    69  			id = class.ID
    70  		})
    71  
    72  		t.Run("retrieving the same classification by id", func(t *testing.T) {
    73  			class, err := classifier.Get(context.Background(), nil, id)
    74  			require.Nil(t, err)
    75  			require.NotNil(t, class)
    76  			assert.Equal(t, id, class.ID)
    77  			assert.Equal(t, models.ClassificationStatusRunning, class.Status)
    78  		})
    79  
    80  		// TODO: improve by polling instead
    81  		time.Sleep(500 * time.Millisecond)
    82  
    83  		t.Run("status is now completed", func(t *testing.T) {
    84  			class, err := classifier.Get(context.Background(), nil, id)
    85  			require.Nil(t, err)
    86  			require.NotNil(t, class)
    87  			assert.Equal(t, models.ClassificationStatusCompleted, class.Status)
    88  		})
    89  
    90  		t.Run("the classifier updated the actions with the classified references", func(t *testing.T) {
    91  			vectorRepo.Lock()
    92  			require.Len(t, vectorRepo.db, 6)
    93  			vectorRepo.Unlock()
    94  
    95  			t.Run("food", func(t *testing.T) {
    96  				idArticleFoodOne := "06a1e824-889c-4649-97f9-1ed3fa401d8e"
    97  				idArticleFoodTwo := "6402e649-b1e0-40ea-b192-a64eab0d5e56"
    98  
    99  				checkRef(t, vectorRepo, idArticleFoodOne, "exactCategory", idCategoryFoodAndDrink)
   100  				checkRef(t, vectorRepo, idArticleFoodTwo, "mainCategory", idMainCategoryFoodAndDrink)
   101  			})
   102  
   103  			t.Run("politics", func(t *testing.T) {
   104  				idArticlePoliticsOne := "75ba35af-6a08-40ae-b442-3bec69b355f9"
   105  				idArticlePoliticsTwo := "f850439a-d3cd-4f17-8fbf-5a64405645cd"
   106  
   107  				checkRef(t, vectorRepo, idArticlePoliticsOne, "exactCategory", idCategoryPolitics)
   108  				checkRef(t, vectorRepo, idArticlePoliticsTwo, "mainCategory", idMainCategoryPoliticsAndSociety)
   109  			})
   110  
   111  			t.Run("society", func(t *testing.T) {
   112  				idArticleSocietyOne := "a2bbcbdc-76e1-477d-9e72-a6d2cfb50109"
   113  				idArticleSocietyTwo := "069410c3-4b9e-4f68-8034-32a066cb7997"
   114  
   115  				checkRef(t, vectorRepo, idArticleSocietyOne, "exactCategory", idCategorySociety)
   116  				checkRef(t, vectorRepo, idArticleSocietyTwo, "mainCategory", idMainCategoryPoliticsAndSociety)
   117  			})
   118  		})
   119  	})
   120  
   121  	t.Run("when errors occur during classification", func(t *testing.T) {
   122  		sg := &fakeSchemaGetter{testSchema()}
   123  		repo := newFakeClassificationRepo()
   124  		authorizer := &fakeAuthorizer{}
   125  		vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified())
   126  		vectorRepo.errorOnAggregate = errors.New("something went wrong")
   127  		classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil)
   128  
   129  		params := models.Classification{
   130  			Class:              "Article",
   131  			BasedOnProperties:  []string{"description"},
   132  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
   133  			Settings: map[string]interface{}{
   134  				"k": json.Number("1"),
   135  			},
   136  		}
   137  
   138  		t.Run("scheduling a classification", func(t *testing.T) {
   139  			class, err := classifier.Schedule(context.Background(), nil, params)
   140  			require.Nil(t, err, "should not error")
   141  			require.NotNil(t, class)
   142  
   143  			assert.Len(t, class.ID, 36, "an id was assigned")
   144  			id = class.ID
   145  		})
   146  
   147  		waitForStatusToNoLongerBeRunning(t, classifier, id)
   148  
   149  		t.Run("status is now failed", func(t *testing.T) {
   150  			class, err := classifier.Get(context.Background(), nil, id)
   151  			require.Nil(t, err)
   152  			require.NotNil(t, class)
   153  			assert.Equal(t, models.ClassificationStatusFailed, class.Status)
   154  			expectedErrStrings := []string{
   155  				"classification failed: ",
   156  				"classify Article/75ba35af-6a08-40ae-b442-3bec69b355f9: something went wrong",
   157  				"classify Article/f850439a-d3cd-4f17-8fbf-5a64405645cd: something went wrong",
   158  				"classify Article/a2bbcbdc-76e1-477d-9e72-a6d2cfb50109: something went wrong",
   159  				"classify Article/069410c3-4b9e-4f68-8034-32a066cb7997: something went wrong",
   160  				"classify Article/06a1e824-889c-4649-97f9-1ed3fa401d8e: something went wrong",
   161  				"classify Article/6402e649-b1e0-40ea-b192-a64eab0d5e56: something went wrong",
   162  			}
   163  
   164  			for _, msg := range expectedErrStrings {
   165  				assert.Contains(t, class.Error, msg)
   166  			}
   167  		})
   168  	})
   169  
   170  	t.Run("when there is nothing to be classified", func(t *testing.T) {
   171  		sg := &fakeSchemaGetter{testSchema()}
   172  		repo := newFakeClassificationRepo()
   173  		authorizer := &fakeAuthorizer{}
   174  		vectorRepo := newFakeVectorRepoKNN(nil, testDataAlreadyClassified())
   175  		classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil)
   176  
   177  		params := models.Classification{
   178  			Class:              "Article",
   179  			BasedOnProperties:  []string{"description"},
   180  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
   181  			Settings: map[string]interface{}{
   182  				"k": json.Number("1"),
   183  			},
   184  		}
   185  
   186  		t.Run("scheduling a classification", func(t *testing.T) {
   187  			class, err := classifier.Schedule(context.Background(), nil, params)
   188  			require.Nil(t, err, "should not error")
   189  			require.NotNil(t, class)
   190  
   191  			assert.Len(t, class.ID, 36, "an id was assigned")
   192  			id = class.ID
   193  		})
   194  
   195  		waitForStatusToNoLongerBeRunning(t, classifier, id)
   196  
   197  		t.Run("status is now failed", func(t *testing.T) {
   198  			class, err := classifier.Get(context.Background(), nil, id)
   199  			require.Nil(t, err)
   200  			require.NotNil(t, class)
   201  			assert.Equal(t, models.ClassificationStatusFailed, class.Status)
   202  			expectedErr := "classification failed: " +
   203  				"no classes to be classified - did you run a previous classification already?"
   204  			assert.Equal(t, expectedErr, class.Error)
   205  		})
   206  	})
   207  }
   208  
   209  func Test_Classifier_Custom_Classifier(t *testing.T) {
   210  	var id strfmt.UUID
   211  	// so we can reuse it for follow up requests, such as checking the status
   212  
   213  	t.Run("with unreconginzed custom module classifier name", func(t *testing.T) {
   214  		sg := &fakeSchemaGetter{testSchema()}
   215  		repo := newFakeClassificationRepo()
   216  		authorizer := &fakeAuthorizer{}
   217  
   218  		vectorRepo := newFakeVectorRepoContextual(testDataToBeClassified(), testDataPossibleTargets())
   219  		logger, _ := test.NewNullLogger()
   220  
   221  		// vectorizer := &fakeVectorizer{words: testDataVectors()}
   222  		modulesProvider := NewFakeModulesProvider()
   223  		classifier := New(sg, repo, vectorRepo, authorizer, logger, modulesProvider)
   224  
   225  		notRecoginzedContextual := "text2vec-contextionary-custom-not-recognized"
   226  		params := models.Classification{
   227  			Class:              "Article",
   228  			BasedOnProperties:  []string{"description"},
   229  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
   230  			Type:               notRecoginzedContextual,
   231  		}
   232  
   233  		t.Run("scheduling an unrecognized classification", func(t *testing.T) {
   234  			class, err := classifier.Schedule(context.Background(), nil, params)
   235  			require.Nil(t, err, "should not error")
   236  			require.NotNil(t, class)
   237  
   238  			assert.Len(t, class.ID, 36, "an id was assigned")
   239  			id = class.ID
   240  		})
   241  
   242  		t.Run("retrieving the same classification by id", func(t *testing.T) {
   243  			class, err := classifier.Get(context.Background(), nil, id)
   244  			require.Nil(t, err)
   245  			require.NotNil(t, class)
   246  			assert.Equal(t, id, class.ID)
   247  		})
   248  
   249  		// TODO: improve by polling instead
   250  		time.Sleep(500 * time.Millisecond)
   251  
   252  		t.Run("status is failed", func(t *testing.T) {
   253  			class, err := classifier.Get(context.Background(), nil, id)
   254  			require.Nil(t, err)
   255  			require.NotNil(t, class)
   256  			assert.Equal(t, models.ClassificationStatusFailed, class.Status)
   257  			assert.Equal(t, notRecoginzedContextual, class.Type)
   258  			assert.Contains(t, class.Error, "classifier "+notRecoginzedContextual+" not found")
   259  		})
   260  	})
   261  
   262  	t.Run("with valid data", func(t *testing.T) {
   263  		sg := &fakeSchemaGetter{testSchema()}
   264  		repo := newFakeClassificationRepo()
   265  		authorizer := &fakeAuthorizer{}
   266  
   267  		vectorRepo := newFakeVectorRepoContextual(testDataToBeClassified(), testDataPossibleTargets())
   268  		logger, _ := test.NewNullLogger()
   269  
   270  		modulesProvider := NewFakeModulesProvider()
   271  		classifier := New(sg, repo, vectorRepo, authorizer, logger, modulesProvider)
   272  
   273  		contextual := "text2vec-contextionary-custom-contextual"
   274  		params := models.Classification{
   275  			Class:              "Article",
   276  			BasedOnProperties:  []string{"description"},
   277  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
   278  			Type:               contextual,
   279  		}
   280  
   281  		t.Run("scheduling a classification", func(t *testing.T) {
   282  			class, err := classifier.Schedule(context.Background(), nil, params)
   283  			require.Nil(t, err, "should not error")
   284  			require.NotNil(t, class)
   285  
   286  			assert.Len(t, class.ID, 36, "an id was assigned")
   287  			id = class.ID
   288  		})
   289  
   290  		t.Run("retrieving the same classification by id", func(t *testing.T) {
   291  			class, err := classifier.Get(context.Background(), nil, id)
   292  			require.Nil(t, err)
   293  			require.NotNil(t, class)
   294  			assert.Equal(t, id, class.ID)
   295  		})
   296  
   297  		// TODO: improve by polling instead
   298  		time.Sleep(500 * time.Millisecond)
   299  
   300  		t.Run("status is now completed", func(t *testing.T) {
   301  			class, err := classifier.Get(context.Background(), nil, id)
   302  			require.Nil(t, err)
   303  			require.NotNil(t, class)
   304  			assert.Equal(t, models.ClassificationStatusCompleted, class.Status)
   305  		})
   306  
   307  		t.Run("the classifier updated the actions with the classified references", func(t *testing.T) {
   308  			vectorRepo.Lock()
   309  			require.Len(t, vectorRepo.db, 6)
   310  			vectorRepo.Unlock()
   311  
   312  			t.Run("food", func(t *testing.T) {
   313  				idArticleFoodOne := "06a1e824-889c-4649-97f9-1ed3fa401d8e"
   314  				idArticleFoodTwo := "6402e649-b1e0-40ea-b192-a64eab0d5e56"
   315  
   316  				checkRef(t, vectorRepo, idArticleFoodOne, "exactCategory", idCategoryFoodAndDrink)
   317  				checkRef(t, vectorRepo, idArticleFoodTwo, "mainCategory", idMainCategoryFoodAndDrink)
   318  			})
   319  
   320  			t.Run("politics", func(t *testing.T) {
   321  				idArticlePoliticsOne := "75ba35af-6a08-40ae-b442-3bec69b355f9"
   322  				idArticlePoliticsTwo := "f850439a-d3cd-4f17-8fbf-5a64405645cd"
   323  
   324  				checkRef(t, vectorRepo, idArticlePoliticsOne, "exactCategory", idCategoryPolitics)
   325  				checkRef(t, vectorRepo, idArticlePoliticsTwo, "mainCategory", idMainCategoryPoliticsAndSociety)
   326  			})
   327  
   328  			t.Run("society", func(t *testing.T) {
   329  				idArticleSocietyOne := "a2bbcbdc-76e1-477d-9e72-a6d2cfb50109"
   330  				idArticleSocietyTwo := "069410c3-4b9e-4f68-8034-32a066cb7997"
   331  
   332  				checkRef(t, vectorRepo, idArticleSocietyOne, "exactCategory", idCategorySociety)
   333  				checkRef(t, vectorRepo, idArticleSocietyTwo, "mainCategory", idMainCategoryPoliticsAndSociety)
   334  			})
   335  		})
   336  	})
   337  
   338  	t.Run("when errors occur during classification", func(t *testing.T) {
   339  		sg := &fakeSchemaGetter{testSchema()}
   340  		repo := newFakeClassificationRepo()
   341  		authorizer := &fakeAuthorizer{}
   342  		vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified())
   343  		vectorRepo.errorOnAggregate = errors.New("something went wrong")
   344  		logger, _ := test.NewNullLogger()
   345  		classifier := New(sg, repo, vectorRepo, authorizer, logger, nil)
   346  
   347  		params := models.Classification{
   348  			Class:              "Article",
   349  			BasedOnProperties:  []string{"description"},
   350  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
   351  			Settings: map[string]interface{}{
   352  				"k": json.Number("1"),
   353  			},
   354  		}
   355  
   356  		t.Run("scheduling a classification", func(t *testing.T) {
   357  			class, err := classifier.Schedule(context.Background(), nil, params)
   358  			require.Nil(t, err, "should not error")
   359  			require.NotNil(t, class)
   360  
   361  			assert.Len(t, class.ID, 36, "an id was assigned")
   362  			id = class.ID
   363  		})
   364  
   365  		waitForStatusToNoLongerBeRunning(t, classifier, id)
   366  
   367  		t.Run("status is now failed", func(t *testing.T) {
   368  			class, err := classifier.Get(context.Background(), nil, id)
   369  			require.Nil(t, err)
   370  			require.NotNil(t, class)
   371  			assert.Equal(t, models.ClassificationStatusFailed, class.Status)
   372  			expectedErrStrings := []string{
   373  				"classification failed: ",
   374  				"classify Article/75ba35af-6a08-40ae-b442-3bec69b355f9: something went wrong",
   375  				"classify Article/f850439a-d3cd-4f17-8fbf-5a64405645cd: something went wrong",
   376  				"classify Article/a2bbcbdc-76e1-477d-9e72-a6d2cfb50109: something went wrong",
   377  				"classify Article/069410c3-4b9e-4f68-8034-32a066cb7997: something went wrong",
   378  				"classify Article/06a1e824-889c-4649-97f9-1ed3fa401d8e: something went wrong",
   379  				"classify Article/6402e649-b1e0-40ea-b192-a64eab0d5e56: something went wrong",
   380  			}
   381  			for _, msg := range expectedErrStrings {
   382  				assert.Contains(t, class.Error, msg)
   383  			}
   384  		})
   385  	})
   386  
   387  	t.Run("when there is nothing to be classified", func(t *testing.T) {
   388  		sg := &fakeSchemaGetter{testSchema()}
   389  		repo := newFakeClassificationRepo()
   390  		authorizer := &fakeAuthorizer{}
   391  		vectorRepo := newFakeVectorRepoKNN(nil, testDataAlreadyClassified())
   392  		logger, _ := test.NewNullLogger()
   393  		classifier := New(sg, repo, vectorRepo, authorizer, logger, nil)
   394  
   395  		params := models.Classification{
   396  			Class:              "Article",
   397  			BasedOnProperties:  []string{"description"},
   398  			ClassifyProperties: []string{"exactCategory", "mainCategory"},
   399  			Settings: map[string]interface{}{
   400  				"k": json.Number("1"),
   401  			},
   402  		}
   403  
   404  		t.Run("scheduling a classification", func(t *testing.T) {
   405  			class, err := classifier.Schedule(context.Background(), nil, params)
   406  			require.Nil(t, err, "should not error")
   407  			require.NotNil(t, class)
   408  
   409  			assert.Len(t, class.ID, 36, "an id was assigned")
   410  			id = class.ID
   411  		})
   412  
   413  		waitForStatusToNoLongerBeRunning(t, classifier, id)
   414  
   415  		t.Run("status is now failed", func(t *testing.T) {
   416  			class, err := classifier.Get(context.Background(), nil, id)
   417  			require.Nil(t, err)
   418  			require.NotNil(t, class)
   419  			assert.Equal(t, models.ClassificationStatusFailed, class.Status)
   420  			expectedErr := "classification failed: " +
   421  				"no classes to be classified - did you run a previous classification already?"
   422  			assert.Equal(t, expectedErr, class.Error)
   423  		})
   424  	})
   425  }
   426  
   427  func Test_Classifier_WhereFilterValidation(t *testing.T) {
   428  	t.Run("when invalid whereFilters are received", func(t *testing.T) {
   429  		sg := &fakeSchemaGetter{testSchema()}
   430  		repo := newFakeClassificationRepo()
   431  		authorizer := &fakeAuthorizer{}
   432  		vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified())
   433  		classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil)
   434  
   435  		t.Run("with only one of the where filters being set", func(t *testing.T) {
   436  			whereFilter := &models.WhereFilter{
   437  				Path:      []string{"id"},
   438  				Operator:  "Like",
   439  				ValueText: ptString("*"),
   440  			}
   441  			testData := []struct {
   442  				name                  string
   443  				classificationType    string
   444  				classificationFilters *models.ClassificationFilters
   445  			}{
   446  				{
   447  					name:               "Contextual only source where filter set",
   448  					classificationType: TypeContextual,
   449  					classificationFilters: &models.ClassificationFilters{
   450  						SourceWhere: whereFilter,
   451  					},
   452  				},
   453  				{
   454  					name:               "Contextual only target where filter set",
   455  					classificationType: TypeContextual,
   456  					classificationFilters: &models.ClassificationFilters{
   457  						TargetWhere: whereFilter,
   458  					},
   459  				},
   460  				{
   461  					name:               "ZeroShot only source where filter set",
   462  					classificationType: TypeZeroShot,
   463  					classificationFilters: &models.ClassificationFilters{
   464  						SourceWhere: whereFilter,
   465  					},
   466  				},
   467  				{
   468  					name:               "ZeroShot only target where filter set",
   469  					classificationType: TypeZeroShot,
   470  					classificationFilters: &models.ClassificationFilters{
   471  						TargetWhere: whereFilter,
   472  					},
   473  				},
   474  				{
   475  					name:               "KNN only source where filter set",
   476  					classificationType: TypeKNN,
   477  					classificationFilters: &models.ClassificationFilters{
   478  						SourceWhere: whereFilter,
   479  					},
   480  				},
   481  				{
   482  					name:               "KNN only training set where filter set",
   483  					classificationType: TypeKNN,
   484  					classificationFilters: &models.ClassificationFilters{
   485  						TrainingSetWhere: whereFilter,
   486  					},
   487  				},
   488  			}
   489  			for _, td := range testData {
   490  				t.Run(td.name, func(t *testing.T) {
   491  					params := models.Classification{
   492  						Class:              "Article",
   493  						BasedOnProperties:  []string{"description"},
   494  						ClassifyProperties: []string{"exactCategory", "mainCategory"},
   495  						Settings: map[string]interface{}{
   496  							"k": json.Number("1"),
   497  						},
   498  						Type:    td.classificationType,
   499  						Filters: td.classificationFilters,
   500  					}
   501  					class, err := classifier.Schedule(context.Background(), nil, params)
   502  					assert.Nil(t, err)
   503  					assert.NotNil(t, class)
   504  
   505  					assert.Len(t, class.ID, 36, "an id was assigned")
   506  					waitForStatusToNoLongerBeRunning(t, classifier, class.ID)
   507  				})
   508  			}
   509  		})
   510  	})
   511  
   512  	t.Run("[deprecated string] when valueString whereFilters are received", func(t *testing.T) {
   513  		sg := &fakeSchemaGetter{testSchema()}
   514  		repo := newFakeClassificationRepo()
   515  		authorizer := &fakeAuthorizer{}
   516  		vectorRepo := newFakeVectorRepoKNN(testDataToBeClassified(), testDataAlreadyClassified())
   517  		classifier := New(sg, repo, vectorRepo, authorizer, newNullLogger(), nil)
   518  
   519  		validFilter := &models.WhereFilter{
   520  			Path:      []string{"description"},
   521  			Operator:  "Equal",
   522  			ValueText: ptString("valueText is valid"),
   523  		}
   524  		deprecatedFilter := &models.WhereFilter{
   525  			Path:        []string{"description"},
   526  			Operator:    "Equal",
   527  			ValueString: ptString("valueString is accepted"),
   528  		}
   529  
   530  		t.Run("with deprecated sourceFilter", func(t *testing.T) {
   531  			params := models.Classification{
   532  				Class:              "Article",
   533  				BasedOnProperties:  []string{"description"},
   534  				ClassifyProperties: []string{"exactCategory", "mainCategory"},
   535  				Settings: map[string]interface{}{
   536  					"k": json.Number("1"),
   537  				},
   538  				Filters: &models.ClassificationFilters{
   539  					SourceWhere: deprecatedFilter,
   540  				},
   541  				Type: TypeContextual,
   542  			}
   543  
   544  			_, err := classifier.Schedule(context.Background(), nil, params)
   545  			assert.Nil(t, err)
   546  		})
   547  
   548  		t.Run("with deprecated targetFilter", func(t *testing.T) {
   549  			params := models.Classification{
   550  				Class:              "Article",
   551  				BasedOnProperties:  []string{"description"},
   552  				ClassifyProperties: []string{"exactCategory", "mainCategory"},
   553  				Settings: map[string]interface{}{
   554  					"k": json.Number("1"),
   555  				},
   556  				Filters: &models.ClassificationFilters{
   557  					SourceWhere: validFilter,
   558  					TargetWhere: deprecatedFilter,
   559  				},
   560  				Type: TypeContextual,
   561  			}
   562  
   563  			_, err := classifier.Schedule(context.Background(), nil, params)
   564  			assert.Nil(t, err)
   565  		})
   566  
   567  		t.Run("with deprecated trainingFilter", func(t *testing.T) {
   568  			params := models.Classification{
   569  				Class:              "Article",
   570  				BasedOnProperties:  []string{"description"},
   571  				ClassifyProperties: []string{"exactCategory", "mainCategory"},
   572  				Settings: map[string]interface{}{
   573  					"k": json.Number("1"),
   574  				},
   575  				Filters: &models.ClassificationFilters{
   576  					SourceWhere:      validFilter,
   577  					TrainingSetWhere: deprecatedFilter,
   578  				},
   579  				Type: TypeKNN,
   580  			}
   581  
   582  			_, err := classifier.Schedule(context.Background(), nil, params)
   583  			assert.Nil(t, err)
   584  		})
   585  	})
   586  }
   587  
   588  type genericFakeRepo interface {
   589  	get(strfmt.UUID) (*models.Object, bool)
   590  }
   591  
   592  func checkRef(t *testing.T, repo genericFakeRepo, source, propName, target string) {
   593  	object, ok := repo.get(strfmt.UUID(source))
   594  	require.True(t, ok, "object must be present")
   595  
   596  	schema, ok := object.Properties.(map[string]interface{})
   597  	require.True(t, ok, "schema must be map")
   598  
   599  	prop, ok := schema[propName]
   600  	require.True(t, ok, "ref prop must be present")
   601  
   602  	refs, ok := prop.(models.MultipleRef)
   603  	require.True(t, ok, "ref prop must be models.MultipleRef")
   604  	require.Len(t, refs, 1, "refs must have len 1")
   605  
   606  	assert.Equal(t, fmt.Sprintf("weaviate://localhost/%s", target), refs[0].Beacon.String(), "beacon must match")
   607  }
   608  
   609  func waitForStatusToNoLongerBeRunning(t *testing.T, classifier *Classifier, id strfmt.UUID) {
   610  	testhelper.AssertEventuallyEqualWithFrequencyAndTimeout(t, true, func() interface{} {
   611  		class, err := classifier.Get(context.Background(), nil, id)
   612  		require.Nil(t, err)
   613  		require.NotNil(t, class)
   614  
   615  		return class.Status != models.ClassificationStatusRunning
   616  	}, 100*time.Millisecond, 20*time.Second, "wait until status in no longer running")
   617  }