tercul-backend/internal/platform/search/weaviate_wrapper_integration_test.go
Damir Mukimov d7390053b9
feat: Apply Jules AI changes - Search service implementation and refactoring
- Implement full-text search service with Weaviate integration
- Remove Bleve search implementation
- Add GraphQL schema files for search, work, author, and translation
- Refactor search domain interfaces
- Update Weaviate wrapper with integration tests
- Clean up unused search client files
2025-11-30 03:15:35 +01:00

331 lines
9.6 KiB
Go

package search_test
import (
"context"
"fmt"
"testing"
"tercul/internal/domain"
domainsearch "tercul/internal/domain/search"
"tercul/internal/platform/search"
"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)
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())
}