package search_test import ( "context" "fmt" "tercul/internal/domain" domainsearch "tercul/internal/domain/search" "tercul/internal/platform/search" "testing" "time" "github.com/go-openapi/strfmt" "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" "github.com/weaviate/weaviate-go-client/v5/weaviate" "github.com/weaviate/weaviate/entities/models" ) type WeaviateWrapperIntegrationTestSuite struct { suite.Suite client *weaviate.Client wrapper domainsearch.SearchClient weaviateContainer testcontainers.Container } func (s *WeaviateWrapperIntegrationTestSuite) SetupSuite() { ctx := context.Background() req := testcontainers.ContainerRequest{ Image: "cr.weaviate.io/semitechnologies/weaviate:1.24.1", ExposedPorts: []string{"8080/tcp"}, Env: map[string]string{ "AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED": "true", "DEFAULT_VECTORIZER_MODULE": "none", "PERSISTENCE_DATA_PATH": "/var/lib/weaviate", }, WaitingFor: wait.ForHTTP("/v1/.well-known/ready").WithPort("8080"), } container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) require.NoError(s.T(), err) s.weaviateContainer = container host, err := container.Host(ctx) require.NoError(s.T(), err) port, err := container.MappedPort(ctx, "8080") require.NoError(s.T(), err) cfg := weaviate.Config{ Host: fmt.Sprintf("%s:%s", host, port.Port()), Scheme: "http", } client, err := weaviate.NewClient(cfg) require.NoError(s.T(), err) s.client = client s.wrapper = search.NewWeaviateWrapper(client, fmt.Sprintf("%s:%s", host, port.Port()), 0.7) s.createTestSchema(ctx) s.seedTestData(ctx) } func (s *WeaviateWrapperIntegrationTestSuite) TearDownSuite() { ctx := context.Background() err := s.client.Schema().AllDeleter().Do(ctx) require.NoError(s.T(), err) require.NoError(s.T(), s.weaviateContainer.Terminate(ctx)) } func (s *WeaviateWrapperIntegrationTestSuite) createTestSchema(ctx context.Context) { workClass := &models.Class{ Class: "Work", Properties: []*models.Property{ {Name: "db_id", DataType: []string{"int"}}, {Name: "title", DataType: []string{"text"}}, {Name: "description", DataType: []string{"text"}}, {Name: "language", DataType: []string{"string"}}, {Name: "status", DataType: []string{"string"}}, {Name: "createdAt", DataType: []string{"date"}}, {Name: "updatedAt", DataType: []string{"date"}}, {Name: "tags", DataType: []string{"string[]"}}, }, } err := s.client.Schema().ClassCreator().WithClass(workClass).Do(ctx) require.NoError(s.T(), err) translationClass := &models.Class{ Class: "Translation", Properties: []*models.Property{ {Name: "db_id", DataType: []string{"int"}}, {Name: "title", DataType: []string{"text"}}, {Name: "content", DataType: []string{"text"}}, {Name: "language", DataType: []string{"string"}}, {Name: "status", DataType: []string{"string"}}, }, } err = s.client.Schema().ClassCreator().WithClass(translationClass).Do(ctx) require.NoError(s.T(), err) authorClass := &models.Class{ Class: "Author", Properties: []*models.Property{ {Name: "db_id", DataType: []string{"int"}}, {Name: "name", DataType: []string{"text"}}, {Name: "biography", DataType: []string{"text"}}, }, } err = s.client.Schema().ClassCreator().WithClass(authorClass).Do(ctx) require.NoError(s.T(), err) } func (s *WeaviateWrapperIntegrationTestSuite) seedTestData(ctx context.Context) { objects := []*models.Object{ { Class: "Work", ID: s.getTestUUID("work-1"), Properties: map[string]interface{}{ "db_id": 1, "title": "The Great Gatsby", "description": "A novel by F. Scott Fitzgerald", "language": "en", "status": "published", "createdAt": time.Now().Add(-24 * time.Hour).Format(time.RFC3339), "updatedAt": time.Now().Format(time.RFC3339), "tags": []string{"classic", "american"}, }, }, { Class: "Work", ID: s.getTestUUID("work-2"), Properties: map[string]interface{}{ "db_id": 2, "title": "War and Peace", "description": "A classic novel by Leo Tolstoy", "language": "ru", "status": "published", "createdAt": time.Now().Add(-48 * time.Hour).Format(time.RFC3339), "updatedAt": time.Now().Format(time.RFC3339), "tags": []string{"classic", "russian"}, }, }, { Class: "Translation", ID: s.getTestUUID("translation-101"), Properties: map[string]interface{}{ "db_id": 101, "title": "Великий Гэтсби", "content": "Содержание перевода...", "language": "ru", "status": "published", }, }, { Class: "Author", ID: s.getTestUUID("author-201"), Properties: map[string]interface{}{ "db_id": 201, "name": "F. Scott Fitzgerald", "biography": "A writer of many a novel.", }, }, } _, err := s.client.Batch().ObjectsBatcher().WithObjects(objects...).Do(ctx) require.NoError(s.T(), err) time.Sleep(1 * time.Second) // Give Weaviate a moment to index } func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_AllEntities() { params := domainsearch.SearchParams{ Query: "novel", Mode: domainsearch.SearchModeBM25, Limit: 10, Offset: 0, } results, err := s.wrapper.Search(context.Background(), params) s.Require().NoError(err) s.Require().NotNil(results) s.Require().Equal(int64(3), results.TotalResults) // Both works and the author's bio contain "novel" s.Require().Len(results.Results, 3) // Check if the correct types are returned foundWork := 0 foundAuthor := 0 for _, item := range results.Results { if item.Type == "Work" { foundWork++ } if item.Type == "Author" { foundAuthor++ } } s.Require().Equal(2, foundWork, "Expected to find two works") s.Require().Equal(1, foundAuthor, "Expected to find one author") } func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_WithLanguageFilter() { params := domainsearch.SearchParams{ Query: "novel", Mode: domainsearch.SearchModeBM25, Limit: 10, Offset: 0, Filters: domainsearch.SearchFilters{ Languages: []string{"ru"}, }, } results, err := s.wrapper.Search(context.Background(), params) s.Require().NoError(err) s.Require().NotNil(results) s.Require().Equal(int64(1), results.TotalResults, "Expected only 'War and Peace' to match") s.Require().Len(results.Results, 1) s.Require().Equal("Work", results.Results[0].Type) work, ok := results.Results[0].Entity.(domain.Work) s.Require().True(ok) s.Require().Equal("War and Peace", work.Title) s.Require().Equal(uint(2), work.ID) } func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_WithTagFilter() { params := domainsearch.SearchParams{ Query: "", // No query, just filtering Mode: domainsearch.SearchModeBM25, Limit: 10, Offset: 0, Filters: domainsearch.SearchFilters{ Types: []string{"Work"}, Tags: []string{"american"}, }, } results, err := s.wrapper.Search(context.Background(), params) s.Require().NoError(err) s.Require().NotNil(results) s.Require().Equal(int64(1), results.TotalResults, "Expected only 'The Great Gatsby' to match") s.Require().Len(results.Results, 1) s.Require().Equal("Work", results.Results[0].Type) work, ok := results.Results[0].Entity.(domain.Work) s.Require().True(ok) s.Require().Equal("The Great Gatsby", work.Title) s.Require().Equal(uint(1), work.ID) } func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_WithAuthorFilter() { params := domainsearch.SearchParams{ Query: "", // No query, just filtering Mode: domainsearch.SearchModeBM25, Limit: 10, Offset: 0, Filters: domainsearch.SearchFilters{ Types: []string{"Author"}, Authors: []string{"F. Scott Fitzgerald"}, }, } results, err := s.wrapper.Search(context.Background(), params) s.Require().NoError(err) s.Require().NotNil(results) s.Require().Equal(int64(1), results.TotalResults) s.Require().Len(results.Results, 1) s.Require().Equal("Author", results.Results[0].Type) author, ok := results.Results[0].Entity.(domain.Author) s.Require().True(ok) s.Require().Equal("F. Scott Fitzgerald", author.Name) s.Require().Equal(uint(201), author.ID) } func (s *WeaviateWrapperIntegrationTestSuite) TestSearch_Pagination() { params := domainsearch.SearchParams{ Query: "novel", Mode: domainsearch.SearchModeBM25, Limit: 1, Offset: 0, } // First page results1, err := s.wrapper.Search(context.Background(), params) s.Require().NoError(err) s.Require().NotNil(results1) s.Require().Equal(int64(3), results1.TotalResults) s.Require().Len(results1.Results, 1) var firstID uint switch e := results1.Results[0].Entity.(type) { case domain.Work: firstID = e.ID case domain.Author: firstID = e.ID default: s.T().Fatalf("Unexpected entity type: %T", e) } // Second page params.Offset = 1 results2, err := s.wrapper.Search(context.Background(), params) s.Require().NoError(err) s.Require().NotNil(results2) s.Require().Equal(int64(3), results2.TotalResults) s.Require().Len(results2.Results, 1) var secondID uint switch e := results2.Results[0].Entity.(type) { case domain.Work: secondID = e.ID case domain.Author: secondID = e.ID default: s.T().Fatalf("Unexpected entity type: %T", e) } s.Require().NotEqual(firstID, secondID, "Paginated results should be different") } func TestWeaviateWrapperIntegrationTestSuite(t *testing.T) { suite.Run(t, new(WeaviateWrapperIntegrationTestSuite)) } func (s *WeaviateWrapperIntegrationTestSuite) getTestUUID(input string) strfmt.UUID { return strfmt.UUID(uuid.NewSHA1(uuid.Nil, []byte(input)).String()) }