github.com/weaviate/weaviate@v1.24.6/test/acceptance/nodes/nodes_api_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 test
    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/client/batch"
    24  	"github.com/weaviate/weaviate/client/meta"
    25  	"github.com/weaviate/weaviate/client/nodes"
    26  	"github.com/weaviate/weaviate/entities/models"
    27  	"github.com/weaviate/weaviate/entities/verbosity"
    28  	"github.com/weaviate/weaviate/test/docker"
    29  	"github.com/weaviate/weaviate/test/helper"
    30  	"github.com/weaviate/weaviate/test/helper/sample-schema/books"
    31  	"github.com/weaviate/weaviate/test/helper/sample-schema/documents"
    32  	"github.com/weaviate/weaviate/test/helper/sample-schema/multishard"
    33  )
    34  
    35  func Test_NodesAPI(t *testing.T) {
    36  	t.Run("empty DB", func(t *testing.T) {
    37  		meta, err := helper.Client(t).Meta.MetaGet(meta.NewMetaGetParams(), nil)
    38  		require.Nil(t, err)
    39  		assert.NotNil(t, meta.GetPayload())
    40  
    41  		assertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
    42  			require.NotNil(t, nodeStatus)
    43  			assert.Equal(t, models.NodeStatusStatusHEALTHY, *nodeStatus.Status)
    44  			assert.True(t, len(nodeStatus.Name) > 0)
    45  			assert.True(t, nodeStatus.GitHash != "" && nodeStatus.GitHash != "unknown")
    46  			assert.Equal(t, meta.Payload.Version, nodeStatus.Version)
    47  			assert.Empty(t, nodeStatus.Shards)
    48  			require.Nil(t, nodeStatus.Stats)
    49  		}
    50  
    51  		testStatusResponse(t, assertions, nil, "")
    52  	})
    53  
    54  	t.Run("DB with Books (1 class ,1 shard configuration, 1 node)", func(t *testing.T) {
    55  		booksClass := books.ClassContextionaryVectorizer()
    56  		helper.CreateClass(t, booksClass)
    57  		defer helper.DeleteClass(t, booksClass.Class)
    58  
    59  		for _, book := range books.Objects() {
    60  			helper.CreateObject(t, book)
    61  			helper.AssertGetObjectEventually(t, book.Class, book.ID)
    62  		}
    63  
    64  		minimalAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
    65  			require.NotNil(t, nodeStatus)
    66  			assert.Equal(t, models.NodeStatusStatusHEALTHY, *nodeStatus.Status)
    67  			assert.True(t, len(nodeStatus.Name) > 0)
    68  			assert.True(t, nodeStatus.GitHash != "" && nodeStatus.GitHash != "unknown")
    69  		}
    70  
    71  		verboseAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
    72  			require.Len(t, nodeStatus.Shards, 1)
    73  			shard := nodeStatus.Shards[0]
    74  			assert.True(t, len(shard.Name) > 0)
    75  			assert.Equal(t, booksClass.Class, shard.Class)
    76  			assert.Equal(t, int64(3), shard.ObjectCount)
    77  			require.NotNil(t, nodeStatus.Stats)
    78  			assert.Equal(t, int64(3), nodeStatus.Stats.ObjectCount)
    79  			assert.Equal(t, int64(1), nodeStatus.Stats.ShardCount)
    80  		}
    81  
    82  		testStatusResponse(t, minimalAssertions, verboseAssertions, "")
    83  	})
    84  
    85  	t.Run("DB with MultiShard (1 class, 2 shards configuration, 1 node)", func(t *testing.T) {
    86  		multiShardClass := multishard.ClassContextionaryVectorizer()
    87  		helper.CreateClass(t, multiShardClass)
    88  		defer helper.DeleteClass(t, multiShardClass.Class)
    89  
    90  		for _, multiShard := range multishard.Objects() {
    91  			helper.CreateObject(t, multiShard)
    92  			helper.AssertGetObjectEventually(t, multiShard.Class, multiShard.ID)
    93  		}
    94  
    95  		minimalAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
    96  			require.NotNil(t, nodeStatus)
    97  			assert.Equal(t, models.NodeStatusStatusHEALTHY, *nodeStatus.Status)
    98  			assert.True(t, len(nodeStatus.Name) > 0)
    99  			assert.True(t, nodeStatus.GitHash != "" && nodeStatus.GitHash != "unknown")
   100  		}
   101  
   102  		verboseAsssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
   103  			assert.Len(t, nodeStatus.Shards, 2)
   104  			for _, shard := range nodeStatus.Shards {
   105  				assert.True(t, len(shard.Name) > 0)
   106  				assert.Equal(t, multiShardClass.Class, shard.Class)
   107  				assert.GreaterOrEqual(t, shard.ObjectCount, int64(0))
   108  				require.NotNil(t, nodeStatus.Stats)
   109  				assert.Equal(t, int64(3), nodeStatus.Stats.ObjectCount)
   110  				assert.Equal(t, int64(2), nodeStatus.Stats.ShardCount)
   111  			}
   112  		}
   113  
   114  		testStatusResponse(t, minimalAssertions, verboseAsssertions, "")
   115  	})
   116  
   117  	t.Run("with class name: DB with Books and Documents, 1 shard, 1 node", func(t *testing.T) {
   118  		booksClass := books.ClassContextionaryVectorizer()
   119  		helper.CreateClass(t, booksClass)
   120  		defer helper.DeleteClass(t, booksClass.Class)
   121  
   122  		t.Run("insert and check books", func(t *testing.T) {
   123  			for _, book := range books.Objects() {
   124  				helper.CreateObject(t, book)
   125  				helper.AssertGetObjectEventually(t, book.Class, book.ID)
   126  			}
   127  
   128  			minimalAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {}
   129  			verboseAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
   130  				require.NotNil(t, nodeStatus.Stats)
   131  				assert.Equal(t, int64(3), nodeStatus.Stats.ObjectCount)
   132  				assert.Equal(t, int64(1), nodeStatus.Stats.ShardCount)
   133  			}
   134  
   135  			testStatusResponse(t, minimalAssertions, verboseAssertions, "")
   136  		})
   137  
   138  		t.Run("insert and check documents", func(t *testing.T) {
   139  			docsClasses := documents.ClassesContextionaryVectorizer(false)
   140  			helper.CreateClass(t, docsClasses[0])
   141  			helper.CreateClass(t, docsClasses[1])
   142  			defer helper.DeleteClass(t, docsClasses[0].Class)
   143  			defer helper.DeleteClass(t, docsClasses[1].Class)
   144  
   145  			for _, doc := range documents.Objects() {
   146  				helper.CreateObject(t, doc)
   147  				helper.AssertGetObjectEventually(t, doc.Class, doc.ID)
   148  			}
   149  
   150  			docsClass := docsClasses[0]
   151  
   152  			minimalAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
   153  				assert.Equal(t, models.NodeStatusStatusHEALTHY, *nodeStatus.Status)
   154  				assert.True(t, len(nodeStatus.Name) > 0)
   155  				assert.True(t, nodeStatus.GitHash != "" && nodeStatus.GitHash != "unknown")
   156  			}
   157  
   158  			verboseAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
   159  				require.NotNil(t, nodeStatus.Stats)
   160  				assert.Equal(t, int64(2), nodeStatus.Stats.ObjectCount)
   161  				assert.Equal(t, int64(1), nodeStatus.Stats.ShardCount)
   162  				assert.Len(t, nodeStatus.Shards, 1)
   163  				shard := nodeStatus.Shards[0]
   164  				assert.True(t, len(shard.Name) > 0)
   165  				assert.Equal(t, docsClass.Class, shard.Class)
   166  				assert.Equal(t, int64(2), shard.ObjectCount)
   167  			}
   168  
   169  			testStatusResponse(t, minimalAssertions, verboseAssertions, docsClass.Class)
   170  		})
   171  	})
   172  
   173  	// This test prevents a regression of
   174  	// https://github.com/weaviate/weaviate/issues/2454
   175  	t.Run("validate count with updates", func(t *testing.T) {
   176  		booksClass := books.ClassContextionaryVectorizer()
   177  		helper.CreateClass(t, booksClass)
   178  		defer helper.DeleteClass(t, booksClass.Class)
   179  
   180  		_, err := helper.BatchClient(t).BatchObjectsCreate(
   181  			batch.NewBatchObjectsCreateParams().WithBody(batch.BatchObjectsCreateBody{
   182  				Objects: []*models.Object{
   183  					{
   184  						ID:    strfmt.UUID("2D0D3E3B-54B2-48D4-BFE0-4BE2C060110E"),
   185  						Class: booksClass.Class,
   186  						Properties: map[string]interface{}{
   187  							"title":       "A book that changes",
   188  							"description": "First iteration",
   189  						},
   190  					},
   191  				},
   192  			}), nil)
   193  		require.Nil(t, err)
   194  
   195  		// Note that this is the same ID as before, so this is an update!!
   196  		_, err = helper.BatchClient(t).BatchObjectsCreate(
   197  			batch.NewBatchObjectsCreateParams().WithBody(batch.BatchObjectsCreateBody{
   198  				Objects: []*models.Object{
   199  					{
   200  						ID:    strfmt.UUID("2D0D3E3B-54B2-48D4-BFE0-4BE2C060110E"),
   201  						Class: booksClass.Class,
   202  						Properties: map[string]interface{}{
   203  							"title":       "A book that changes",
   204  							"description": "A new (second) iteration",
   205  						},
   206  					},
   207  				},
   208  			}), nil)
   209  		require.Nil(t, err)
   210  
   211  		minimalAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {}
   212  		verboseAssertions := func(t require.TestingT, nodeStatus *models.NodeStatus) {
   213  			require.NotNil(t, nodeStatus.Stats)
   214  			assert.Equal(t, int64(1), nodeStatus.Stats.ObjectCount)
   215  		}
   216  
   217  		testStatusResponse(t, minimalAssertions, verboseAssertions, "")
   218  	})
   219  }
   220  
   221  func TestNodesApi_Compression_AsyncIndexing(t *testing.T) {
   222  	ctx := context.Background()
   223  	compose, err := docker.New().
   224  		WithWeaviate().
   225  		WithText2VecContextionary().
   226  		WithWeaviateEnv("ASYNC_INDEXING", "true").
   227  		Start(ctx)
   228  	require.NoError(t, err)
   229  	defer func() {
   230  		require.NoError(t, compose.Terminate(ctx))
   231  	}()
   232  
   233  	defer helper.SetupClient(fmt.Sprintf("%s:%s", helper.ServerHost, helper.ServerPort))
   234  	helper.SetupClient(compose.GetWeaviate().URI())
   235  
   236  	t.Run("validate flat compression status", func(t *testing.T) {
   237  		booksClass := books.ClassContextionaryVectorizer()
   238  		booksClass.VectorIndexType = "flat"
   239  		booksClass.VectorIndexConfig = map[string]interface{}{
   240  			"bq": map[string]interface{}{
   241  				"enabled": true,
   242  			},
   243  		}
   244  		helper.CreateClass(t, booksClass)
   245  		defer helper.DeleteClass(t, booksClass.Class)
   246  
   247  		t.Run("check compressed true", func(t *testing.T) {
   248  			verbose := "verbose"
   249  			params := nodes.NewNodesGetParams().WithOutput(&verbose)
   250  			resp, err := helper.Client(t).Nodes.NodesGet(params, nil)
   251  			require.Nil(t, err)
   252  
   253  			nodeStatusResp := resp.GetPayload()
   254  			require.NotNil(t, nodeStatusResp)
   255  
   256  			nodes := nodeStatusResp.Nodes
   257  			require.NotNil(t, nodes)
   258  			require.Len(t, nodes, 1)
   259  
   260  			nodeStatus := nodes[0]
   261  			require.NotNil(t, nodeStatus)
   262  
   263  			require.True(t, nodeStatus.Shards[0].Compressed)
   264  		})
   265  	})
   266  
   267  	t.Run("validate hnsw pq async compression", func(t *testing.T) {
   268  		booksClass := books.ClassContextionaryVectorizer()
   269  		booksClass.VectorIndexConfig = map[string]interface{}{
   270  			"pq": map[string]interface{}{
   271  				"trainingLimit": 256,
   272  				"enabled":       true,
   273  				"segments":      1,
   274  			},
   275  		}
   276  		helper.CreateClass(t, booksClass)
   277  		defer helper.DeleteClass(t, booksClass.Class)
   278  
   279  		t.Run("check compressed initially false", func(t *testing.T) {
   280  			verbose := "verbose"
   281  			params := nodes.NewNodesGetParams().WithOutput(&verbose)
   282  			resp, err := helper.Client(t).Nodes.NodesGet(params, nil)
   283  			require.Nil(t, err)
   284  
   285  			nodeStatusResp := resp.GetPayload()
   286  			require.NotNil(t, nodeStatusResp)
   287  
   288  			nodes := nodeStatusResp.Nodes
   289  			require.NotNil(t, nodes)
   290  			require.Len(t, nodes, 1)
   291  
   292  			nodeStatus := nodes[0]
   293  			require.NotNil(t, nodeStatus)
   294  
   295  			require.False(t, nodeStatus.Shards[0].Compressed)
   296  		})
   297  
   298  		t.Run("load data for pq", func(t *testing.T) {
   299  			num := 1024
   300  			objects := make([]*models.Object, num)
   301  
   302  			for i := 0; i < num; i++ {
   303  				objects[i] = &models.Object{
   304  					Class:  booksClass.Class,
   305  					Vector: []float32{float32(i % 32), float32(i), 3.0, 4.0},
   306  				}
   307  			}
   308  
   309  			_, err := helper.BatchClient(t).BatchObjectsCreate(
   310  				batch.NewBatchObjectsCreateParams().WithBody(batch.BatchObjectsCreateBody{
   311  					Objects: objects,
   312  				},
   313  				), nil)
   314  			require.Nil(t, err)
   315  		})
   316  
   317  		t.Run("check eventually compressed if async enabled", func(t *testing.T) {
   318  			checkThunk := func() interface{} {
   319  				verbose := "verbose"
   320  				params := nodes.NewNodesGetParams().WithOutput(&verbose)
   321  				resp, err := helper.Client(t).Nodes.NodesGet(params, nil)
   322  				require.Nil(t, err)
   323  
   324  				nodeStatusResp := resp.GetPayload()
   325  				require.NotNil(t, nodeStatusResp)
   326  
   327  				nodes := nodeStatusResp.Nodes
   328  				require.NotNil(t, nodes)
   329  				require.Len(t, nodes, 1)
   330  
   331  				nodeStatus := nodes[0]
   332  				require.NotNil(t, nodeStatus)
   333  				return nodeStatus.Shards[0].Compressed
   334  			}
   335  
   336  			helper.AssertEventuallyEqualWithFrequencyAndTimeout(t, true, checkThunk, 100*time.Millisecond, 10*time.Second)
   337  		})
   338  	})
   339  }
   340  
   341  func TestNodesApi_Compression_SyncIndexing(t *testing.T) {
   342  	t.Run("validate flat compression status", func(t *testing.T) {
   343  		booksClass := books.ClassContextionaryVectorizer()
   344  		booksClass.VectorIndexType = "flat"
   345  		booksClass.VectorIndexConfig = map[string]interface{}{
   346  			"bq": map[string]interface{}{
   347  				"enabled": true,
   348  			},
   349  		}
   350  		helper.CreateClass(t, booksClass)
   351  		defer helper.DeleteClass(t, booksClass.Class)
   352  
   353  		t.Run("check compressed true", func(t *testing.T) {
   354  			verbose := "verbose"
   355  			params := nodes.NewNodesGetParams().WithOutput(&verbose)
   356  			resp, err := helper.Client(t).Nodes.NodesGet(params, nil)
   357  			require.Nil(t, err)
   358  
   359  			nodeStatusResp := resp.GetPayload()
   360  			require.NotNil(t, nodeStatusResp)
   361  
   362  			nodes := nodeStatusResp.Nodes
   363  			require.NotNil(t, nodes)
   364  			require.Len(t, nodes, 1)
   365  
   366  			nodeStatus := nodes[0]
   367  			require.NotNil(t, nodeStatus)
   368  
   369  			require.True(t, nodeStatus.Shards[0].Compressed)
   370  		})
   371  	})
   372  }
   373  
   374  func testStatusResponse(t *testing.T, minimalAssertions, verboseAssertions func(require.TestingT, *models.NodeStatus),
   375  	class string,
   376  ) {
   377  	minimal, verbose := verbosity.OutputMinimal, verbosity.OutputVerbose
   378  
   379  	commonTests := func(resp *nodes.NodesGetOK) {
   380  		require.NotNil(t, resp.Payload)
   381  		nodes := resp.Payload.Nodes
   382  		require.NotNil(t, nodes)
   383  		require.Len(t, nodes, 1)
   384  		minimalAssertions(t, nodes[0])
   385  	}
   386  
   387  	t.Run("minimal", func(t *testing.T) {
   388  		payload, err := getNodesStatus(t, minimal, class)
   389  		require.Nil(t, err)
   390  		commonTests(&nodes.NodesGetOK{Payload: payload})
   391  	})
   392  
   393  	if verboseAssertions != nil {
   394  		t.Run("verbose", func(t *testing.T) {
   395  			getNodes := func() (*models.NodesStatusResponse, error) {
   396  				return getNodesStatus(t, verbose, class)
   397  			}
   398  			assert.EventuallyWithT(t, func(t *assert.CollectT) {
   399  				payload, err := getNodes()
   400  				require.Nil(t, err)
   401  				commonTests(&nodes.NodesGetOK{Payload: payload})
   402  				// If commonTests pass, resp.Nodes[0] != nil
   403  				verboseAssertions(t, payload.Nodes[0])
   404  			}, 15*time.Second, 500*time.Millisecond)
   405  		})
   406  	}
   407  }
   408  
   409  func getNodesStatus(t *testing.T, output, class string) (payload *models.NodesStatusResponse, err error) {
   410  	if class != "" {
   411  		params := nodes.NewNodesGetClassParams().WithOutput(&output).WithClassName(class)
   412  		body, clientErr := helper.Client(t).Nodes.NodesGetClass(params, nil)
   413  		payload, err = body.Payload, clientErr
   414  	} else {
   415  		params := nodes.NewNodesGetParams().WithOutput(&output)
   416  		body, clientErr := helper.Client(t).Nodes.NodesGet(params, nil)
   417  		payload, err = body.Payload, clientErr
   418  	}
   419  	return
   420  }