diff --git a/server/channels/store/searchtest/helper.go b/server/channels/store/searchtest/helper.go index d9bbf3dea76..ade539e6782 100644 --- a/server/channels/store/searchtest/helper.go +++ b/server/channels/store/searchtest/helper.go @@ -31,12 +31,6 @@ type SearchTestHelper struct { } func (th *SearchTestHelper) SetupBasicFixtures() error { - // Remove users from previous tests - err := th.cleanAllUsers() - if err != nil { - return err - } - // Create teams team, err := th.createTeam("searchtest-team", "Searchtest team", model.TeamOpen) if err != nil { @@ -140,7 +134,7 @@ func (th *SearchTestHelper) CleanFixtures() error { return err } - err = th.cleanAllUsers() + err = th.cleanAllUsers([]*model.User{th.User, th.User2, th.UserAnotherTeam}) if err != nil { return err } @@ -202,12 +196,7 @@ func (th *SearchTestHelper) deleteBotUser(botID string) error { return th.Store.User().PermanentDelete(th.Context, botID) } -func (th *SearchTestHelper) cleanAllUsers() error { - users, err := th.Store.User().GetAll() - if err != nil { - return err - } - +func (th *SearchTestHelper) cleanAllUsers(users []*model.User) error { for _, u := range users { err := th.deleteUser(u) if err != nil { diff --git a/server/enterprise/elasticsearch/common/common.go b/server/enterprise/elasticsearch/common/common.go index 7d3bec19b6c..4dcdcbf29c0 100644 --- a/server/enterprise/elasticsearch/common/common.go +++ b/server/enterprise/elasticsearch/common/common.go @@ -265,6 +265,17 @@ func ESUserFromUserForIndexing(userForIndexing *model.UserForIndexing) *ESUser { return ESUserFromUserAndTeams(user, userForIndexing.TeamsIds, userForIndexing.ChannelsIds) } +// SearchIndexName returns the index pattern to search for a given index name. +func SearchIndexName(settings model.ElasticsearchSettings, name string) string { + if *settings.GlobalSearchPrefix == "" { + return *settings.IndexPrefix + name + } + + // GlobalSearchPrefix is a prefix of IndexPrefix itself. This is verified in the config. + // Therefore, we use * to search across all indices with the common search prefix. + return *settings.GlobalSearchPrefix + "*" + name +} + func BuildPostIndexName(aggregateAfterDays int, unaggregatedBase string, aggregatedBase string, now time.Time, createAt int64) string { postTime := time.Unix(createAt/1000, 0) aggregateCutoffTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, -aggregateAfterDays+1) diff --git a/server/enterprise/elasticsearch/common/test_suite.go b/server/enterprise/elasticsearch/common/test_suite.go index e29b51dee0f..b624d3d4187 100644 --- a/server/enterprise/elasticsearch/common/test_suite.go +++ b/server/enterprise/elasticsearch/common/test_suite.go @@ -381,6 +381,190 @@ func (c *CommonTestSuite) TestIndexUser() { c.True(found) } +func (c *CommonTestSuite) TestSearchUsersInChannel() { + // Create test channels + channel1 := createChannel(c.TH.BasicTeam.Id, "channel1", "Test Channel 1", model.ChannelTypeOpen) + c.Nil(c.ESImpl.IndexChannel(c.TH.Context, channel1, []string{}, []string{})) + + channel2 := createChannel(c.TH.BasicTeam.Id, "channel2", "Test Channel 2", model.ChannelTypeOpen) + c.Nil(c.ESImpl.IndexChannel(c.TH.Context, channel2, []string{}, []string{})) + + // Create and index users with different channel memberships + user1 := createUser("test.user1", "testuser1", "Test", "User1") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user1, []string{c.TH.BasicTeam.Id}, []string{channel1.Id})) + + user2 := createUser("test.user2", "testuser2", "Test", "User2") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user2, []string{c.TH.BasicTeam.Id}, []string{channel1.Id, channel2.Id})) + + user3 := createUser("test.user3", "testuser3", "Another", "User3") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user3, []string{c.TH.BasicTeam.Id}, []string{channel2.Id})) + + // Wait for indexing to complete + c.NoError(c.RefreshIndexFn()) + + // Search options + options := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 100, + } + + c.Run("All users in channel1", func() { + // Test 1: Search for all users in channel1 + inChannel, notInChannel, err := c.ESImpl.SearchUsersInChannel(c.TH.BasicTeam.Id, channel1.Id, nil, "", options) + c.Nil(err) + c.Len(inChannel, 2) + c.Contains(inChannel, user1.Id) + c.Contains(inChannel, user2.Id) + c.Len(notInChannel, 1) + c.Contains(notInChannel, user3.Id) + }) + + c.Run("Search for specific user in channel1", func() { + // Test 2: Search with term that should match user1 in channel1 + inChannel, notInChannel, err := c.ESImpl.SearchUsersInChannel(c.TH.BasicTeam.Id, channel1.Id, nil, "testuser1", options) + c.Nil(err) + c.Len(inChannel, 1) + c.Contains(inChannel, user1.Id) + c.Empty(notInChannel) + }) + + c.Run("Search with restricted channels", func() { + // Test 3: Search with restrictedToChannels, user3 should be in notInChannel + restrictedToChannels := []string{channel2.Id} + inChannel, notInChannel, err := c.ESImpl.SearchUsersInChannel(c.TH.BasicTeam.Id, channel1.Id, restrictedToChannels, "", options) + c.Nil(err) + c.Len(inChannel, 2) + c.Contains(inChannel, user1.Id) + c.Contains(inChannel, user2.Id) + c.Len(notInChannel, 1) + c.Contains(notInChannel, user3.Id) // user3 is in channel2 but not channel1 + }) + + c.Run("Search with term in restricted channels", func() { + // Test 4: Search with a term in restrictedToChannels + restrictedToChannels := []string{channel2.Id} + inChannel, notInChannel, err := c.ESImpl.SearchUsersInChannel(c.TH.BasicTeam.Id, channel1.Id, restrictedToChannels, "another", options) + c.Nil(err) + c.Empty(inChannel) // No users in channel1 match "another" + c.Len(notInChannel, 1) + c.Contains(notInChannel, user3.Id) // user3's name contains "Another" and is in channel2 + }) + + c.Run("Search with empty restricted channels", func() { + // Test 5: Search with restrictedToChannels but empty (should return no results) + emptyRestricted := []string{} + inChannel, notInChannel, err := c.ESImpl.SearchUsersInChannel(c.TH.BasicTeam.Id, channel1.Id, emptyRestricted, "", options) + c.Nil(err) + c.Empty(inChannel) + c.Empty(notInChannel) + }) + + // Clean up + c.Nil(c.ESImpl.DeleteUser(user1)) + c.Nil(c.ESImpl.DeleteUser(user2)) + c.Nil(c.ESImpl.DeleteUser(user3)) + c.Nil(c.ESImpl.DeleteChannel(channel1)) + c.Nil(c.ESImpl.DeleteChannel(channel2)) +} + +func (c *CommonTestSuite) TestSearchUsersInTeam() { + // Create additional teams + team1 := c.TH.CreateTeam() + team2 := c.TH.CreateTeam() + + // Create and index users with different team memberships + user1 := createUser("test.user1", "testuser1", "Test", "User1") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user1, []string{team1.Id}, []string{})) + + user2 := createUser("test.user2", "testuser2", "Test", "User2") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user2, []string{team1.Id, team2.Id}, []string{})) + + user3 := createUser("test.user3", "testuser3", "Another", "User3") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user3, []string{team2.Id}, []string{})) + + // Wait for indexing to complete + c.NoError(c.RefreshIndexFn()) + + // Search options + options := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 100, + } + + c.Run("Search for all users in team1", func() { + // Test 1: Search for all users in team1 + userIds, err := c.ESImpl.SearchUsersInTeam(team1.Id, nil, "testuser", options) + c.Nil(err) + c.Len(userIds, 2) + c.Contains(userIds, user1.Id) + c.Contains(userIds, user2.Id) + c.NotContains(userIds, user3.Id) + }) + + c.Run("Search for specific user in team1", func() { + // Test 2: Search with term that should match user1 in team1 + userIds, err := c.ESImpl.SearchUsersInTeam(team1.Id, nil, "testuser1", options) + c.Nil(err) + c.Len(userIds, 1) + c.Contains(userIds, user1.Id) + c.NotContains(userIds, user2.Id) + c.NotContains(userIds, user3.Id) + }) + + c.Run("Search in team2", func() { + // Test 3: Search in team2 + userIds, err := c.ESImpl.SearchUsersInTeam(team2.Id, nil, "testuser", options) + c.Nil(err) + c.Len(userIds, 2) + c.Contains(userIds, user2.Id) + c.Contains(userIds, user3.Id) + c.NotContains(userIds, user1.Id) + }) + + c.Run("Search with term in team2", func() { + // Test 4: Search with term in team2 + userIds, err := c.ESImpl.SearchUsersInTeam(team2.Id, nil, "another", options) + c.Nil(err) + c.Len(userIds, 1) + c.Contains(userIds, user3.Id) + c.NotContains(userIds, user1.Id) + c.NotContains(userIds, user2.Id) + }) + + c.Run("Search with restrictedToChannels", func() { + // Test 5: Search with restrictedToChannels + // Create channel in team1 + channel1 := createChannel(team1.Id, "channel1", "Test Channel 1", model.ChannelTypeOpen) + c.Nil(c.ESImpl.IndexChannel(c.TH.Context, channel1, []string{}, []string{})) + + // Update user1 to be in channel1 + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user1, []string{team1.Id}, []string{channel1.Id})) + c.NoError(c.RefreshIndexFn()) + + // Search for users in team1 restricted to channel1 + userIds, err := c.ESImpl.SearchUsersInTeam(team1.Id, []string{channel1.Id}, "", options) + c.Nil(err) + c.Len(userIds, 1) + c.Contains(userIds, user1.Id) + c.NotContains(userIds, user2.Id) + c.NotContains(userIds, user3.Id) + c.Nil(c.ESImpl.DeleteChannel(channel1)) + }) + + c.Run("Search with empty restrictedToChannels", func() { + // Test 6: Search with empty restrictedToChannels (should return no results) + emptyRestricted := []string{} + userIds, err := c.ESImpl.SearchUsersInTeam(team1.Id, emptyRestricted, "", options) + c.Nil(err) + c.Empty(userIds) + }) + + // Clean up + c.Nil(c.ESImpl.DeleteUser(user1)) + c.Nil(c.ESImpl.DeleteUser(user2)) + c.Nil(c.ESImpl.DeleteUser(user3)) +} + func (c *CommonTestSuite) TestDeleteUser() { // Create and index a user user := createUser("test.user", "testuser", "Test", "User") @@ -553,6 +737,264 @@ func (c *CommonTestSuite) TestDeletePostFiles() { c.False(found) } +func (c *CommonTestSuite) TestSearchFiles() { + // First, create and index a channel + channel := createChannel(c.TH.BasicTeam.Id, "channel", "Test Channel", model.ChannelTypeOpen) + c.Nil(c.ESImpl.IndexChannel(c.TH.Context, channel, []string{}, []string{})) + + // Then, create and index a user + user := createUser("test.user", "testuser", "Test", "User") + c.Nil(c.ESImpl.IndexUser(c.TH.Context, user, []string{c.TH.BasicTeam.Id}, []string{channel.Id})) + + // Create multiple test files with different content + file1 := createFile(user.Id, channel.Id, "", "apple document content", "apple_report", "txt") + c.Nil(c.ESImpl.IndexFile(file1, channel.Id)) + + file2 := createFile(user.Id, channel.Id, "", "orange presentation content", "orange_slides", "pdf") + c.Nil(c.ESImpl.IndexFile(file2, channel.Id)) + + file3 := createFile(user.Id, channel.Id, "", "banana data content", "banana_sheet", "xls") + c.Nil(c.ESImpl.IndexFile(file3, channel.Id)) + + // Wait for indexing to complete + c.NoError(c.RefreshIndexFn()) + + // Create channel list for search + channels := model.ChannelList{channel} + + c.Run("Search by term (file name and content)", func() { + // Test 1: Search by term (file name and content) + searchParams := []*model.SearchParams{ + { + Terms: "apple", + IsHashtag: false, + OrTerms: false, + }, + } + + fileIds, err := c.ESImpl.SearchFiles(channels, searchParams, 0, 10) + c.Nil(err) + c.Contains(fileIds, file1.Id) + c.NotContains(fileIds, file2.Id) + c.NotContains(fileIds, file3.Id) + }) + + c.Run("Search by extension", func() { + // Test 2: Search by extension + searchParams := []*model.SearchParams{ + { + Terms: "", + IsHashtag: false, + OrTerms: false, + Extensions: []string{"pdf"}, + }, + } + + fileIds, err := c.ESImpl.SearchFiles(channels, searchParams, 0, 10) + c.Nil(err) + c.NotContains(fileIds, file1.Id) + c.Contains(fileIds, file2.Id) + c.NotContains(fileIds, file3.Id) + }) + + c.Run("Search with OR terms", func() { + // Test 3: Search with OR terms + searchParams := []*model.SearchParams{ + { + Terms: "apple banana", + IsHashtag: false, + OrTerms: true, + }, + } + + fileIds, err := c.ESImpl.SearchFiles(channels, searchParams, 0, 10) + c.Nil(err) + c.Contains(fileIds, file1.Id) + c.NotContains(fileIds, file2.Id) + c.Contains(fileIds, file3.Id) + }) + + c.Run("Search with excluded terms", func() { + // Test 4: Search with excluded terms + searchParams := []*model.SearchParams{ + { + Terms: "content", + ExcludedTerms: "orange", + IsHashtag: false, + OrTerms: false, + }, + } + + fileIds, err := c.ESImpl.SearchFiles(channels, searchParams, 0, 10) + c.Nil(err) + c.Contains(fileIds, file1.Id) + c.NotContains(fileIds, file2.Id) + c.Contains(fileIds, file3.Id) + }) + + // Clean up indexed files + c.Nil(c.ESImpl.DeleteFile(file1.Id)) + c.Nil(c.ESImpl.DeleteFile(file2.Id)) + c.Nil(c.ESImpl.DeleteFile(file3.Id)) +} + +func (c *CommonTestSuite) TestSearchMultiDC() { + // Store original settings to restore later + originalIndexPrefix := *c.TH.App.Config().ElasticsearchSettings.IndexPrefix + originalGlobalSearchPrefix := *c.TH.App.Config().ElasticsearchSettings.GlobalSearchPrefix + + defer c.TH.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ElasticsearchSettings.IndexPrefix = originalIndexPrefix + *cfg.ElasticsearchSettings.GlobalSearchPrefix = originalGlobalSearchPrefix + }) + + // First using DC1 prefix + c.TH.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ElasticsearchSettings.IndexPrefix = "test_dc1_" + *cfg.ElasticsearchSettings.GlobalSearchPrefix = "" + }) + + // Create a post with content specific to DC1 + postDC1 := createPost(c.TH.BasicUser.Id, c.TH.BasicChannel.Id, "unique apple datacenter1") + c.Nil(c.ESImpl.IndexPost(postDC1, c.TH.BasicTeam.Id)) + + // Now switch to DC2 prefix + c.TH.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ElasticsearchSettings.IndexPrefix = "test_dc2_" + *cfg.ElasticsearchSettings.GlobalSearchPrefix = "" + }) + + // Create a post with content specific to DC2 + postDC2 := createPost(c.TH.BasicUser.Id, c.TH.BasicChannel.Id, "unique banana datacenter2") + c.Nil(c.ESImpl.IndexPost(postDC2, c.TH.BasicTeam.Id)) + + // Ensure posts are indexed + c.NoError(c.RefreshIndexFn()) + + channels := model.ChannelList{c.TH.BasicChannel} + + // First verify each prefix only finds its own posts + c.Run("DC1 prefix only finds DC1 post", func() { + c.TH.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ElasticsearchSettings.IndexPrefix = "test_dc1_" + *cfg.ElasticsearchSettings.GlobalSearchPrefix = "" + }) + + // Search for common term + searchParams := []*model.SearchParams{ + { + Terms: "unique", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err := c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Contains(postIds, postDC1.Id) + c.NotContains(postIds, postDC2.Id) + }) + + c.Run("DC2 prefix only finds DC2 post", func() { + c.TH.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ElasticsearchSettings.IndexPrefix = "test_dc2_" + *cfg.ElasticsearchSettings.GlobalSearchPrefix = "" + }) + + // Search for common term + searchParams := []*model.SearchParams{ + { + Terms: "unique", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err := c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Contains(postIds, postDC2.Id) + c.NotContains(postIds, postDC1.Id) + }) + + c.Run("Global prefix finds posts from both DCs", func() { + // Set global search prefix to search across both indices + c.TH.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ElasticsearchSettings.IndexPrefix = "test_dc1_" // Specific prefix doesn't matter for this test + *cfg.ElasticsearchSettings.GlobalSearchPrefix = "test_" + }) + + // Search for common term - should find both posts + searchParams := []*model.SearchParams{ + { + Terms: "unique", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err := c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Len(postIds, 2) + c.Contains(postIds, postDC1.Id) + c.Contains(postIds, postDC2.Id) + + // Search for DC1-specific content + searchParams = []*model.SearchParams{ + { + Terms: "apple", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err = c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Contains(postIds, postDC1.Id) + c.NotContains(postIds, postDC2.Id) + + // Search for DC2-specific content + searchParams = []*model.SearchParams{ + { + Terms: "banana", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err = c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Contains(postIds, postDC2.Id) + c.NotContains(postIds, postDC1.Id) + + // Search for datacenter-specific content + searchParams = []*model.SearchParams{ + { + Terms: "datacenter1", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err = c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Contains(postIds, postDC1.Id) + c.NotContains(postIds, postDC2.Id) + + searchParams = []*model.SearchParams{ + { + Terms: "datacenter2", + IsHashtag: false, + OrTerms: false, + }, + } + + postIds, _, err = c.ESImpl.SearchPosts(channels, searchParams, 0, 20) + c.Nil(err) + c.Contains(postIds, postDC2.Id) + c.NotContains(postIds, postDC1.Id) + }) +} + func (c *CommonTestSuite) TestElasticsearchDataRetentionDeleteIndexes() { c.Nil(c.CreateIndexFn("posts_2017_09_15")) c.Nil(c.CreateIndexFn("posts_2017_09_16")) diff --git a/server/enterprise/elasticsearch/elasticsearch/elasticsearch.go b/server/enterprise/elasticsearch/elasticsearch/elasticsearch.go index ece15b8128b..12b722764a7 100644 --- a/server/enterprise/elasticsearch/elasticsearch/elasticsearch.go +++ b/server/enterprise/elasticsearch/elasticsearch/elasticsearch.go @@ -549,7 +549,7 @@ func (es *ElasticsearchInterfaceImpl) SearchPosts(channels model.ChannelList, se } search := es.client.Search(). - Index(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts + "*"). + Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBasePosts+"*")). Request(&search.Request{ Query: query, Highlight: highlight, @@ -822,7 +822,7 @@ func (es *ElasticsearchInterfaceImpl) SearchChannels(teamId, userID string, term } search := es.client.Search(). - Index(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseChannels). + Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseChannels)). Request(&search.Request{ Query: &types.Query{Bool: query}, }). @@ -996,7 +996,7 @@ func (es *ElasticsearchInterfaceImpl) autocompleteUsers(contextCategory string, } search := es.client.Search(). - Index(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers). + Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseUsers)). Request(&search.Request{ Query: &types.Query{Bool: query}, }). @@ -1116,7 +1116,7 @@ func (es *ElasticsearchInterfaceImpl) autocompleteUsersNotInChannel(teamId, chan } search := es.client.Search(). - Index(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers). + Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseUsers)). Request(&search.Request{ Query: &types.Query{Bool: query}, }). @@ -1664,7 +1664,7 @@ func (es *ElasticsearchInterfaceImpl) SearchFiles(channels model.ChannelList, se } search := es.client.Search(). - Index(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles). + Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseFiles)). Request(&search.Request{ Query: query, }). diff --git a/server/enterprise/elasticsearch/opensearch/opensearch.go b/server/enterprise/elasticsearch/opensearch/opensearch.go index 69f85f9f4d0..99c1b56d661 100644 --- a/server/enterprise/elasticsearch/opensearch/opensearch.go +++ b/server/enterprise/elasticsearch/opensearch/opensearch.go @@ -618,7 +618,7 @@ func (os *OpensearchInterfaceImpl) SearchPosts(channels model.ChannelList, searc var searchResult searchResp _, err = os.client.Client.Do(ctx, &opensearchapi.SearchReq{ - Indices: []string{*os.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts + "*"}, + Indices: []string{common.SearchIndexName(os.Platform.Config().ElasticsearchSettings, common.IndexBasePosts+"*")}, Body: bytes.NewReader(searchBuf), Params: opensearchapi.SearchParams{ From: model.NewPointer(page * perPage), @@ -910,7 +910,7 @@ func (os *OpensearchInterfaceImpl) SearchChannels(teamId, userID string, term st return []string{}, model.NewAppError("Opensearch.SearchChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err) } searchResult, err := os.client.Search(ctx, &opensearchapi.SearchReq{ - Indices: []string{*os.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseChannels}, + Indices: []string{common.SearchIndexName(os.Platform.Config().ElasticsearchSettings, common.IndexBaseChannels)}, Body: bytes.NewReader(buf), Params: opensearchapi.SearchParams{ Size: model.NewPointer(model.ChannelSearchDefaultLimit), @@ -1097,7 +1097,7 @@ func (os *OpensearchInterfaceImpl) autocompleteUsers(contextCategory string, cat } searchResults, err := os.client.Search(ctx, &opensearchapi.SearchReq{ - Indices: []string{*os.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers}, + Indices: []string{common.SearchIndexName(os.Platform.Config().ElasticsearchSettings, common.IndexBaseUsers)}, Body: bytes.NewReader(buf), Params: opensearchapi.SearchParams{ Size: model.NewPointer(options.Limit), @@ -1223,7 +1223,7 @@ func (os *OpensearchInterfaceImpl) autocompleteUsersNotInChannel(teamId, channel } searchResults, err := os.client.Search(ctx, &opensearchapi.SearchReq{ - Indices: []string{*os.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers}, + Indices: []string{common.SearchIndexName(os.Platform.Config().ElasticsearchSettings, common.IndexBaseUsers)}, Body: bytes.NewReader(buf), Params: opensearchapi.SearchParams{ Size: model.NewPointer(options.Limit), @@ -1798,7 +1798,7 @@ func (os *OpensearchInterfaceImpl) SearchFiles(channels model.ChannelList, searc } searchResult, err := os.client.Search(ctx, &opensearchapi.SearchReq{ - Indices: []string{*os.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles}, + Indices: []string{common.SearchIndexName(os.Platform.Config().ElasticsearchSettings, common.IndexBaseFiles)}, Body: bytes.NewReader(searchBuf), Params: opensearchapi.SearchParams{ From: model.NewPointer(page * perPage), diff --git a/server/i18n/en.json b/server/i18n/en.json index c6eb59669c7..203bbf20b73 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -8952,6 +8952,10 @@ "id": "model.config.is_valid.elastic_search.connection_url.app_error", "translation": "Search ConnectionUrl setting must be provided when indexing is enabled." }, + { + "id": "model.config.is_valid.elastic_search.empty_index_prefix.app_error", + "translation": "IndexPrefix cannot be empty if GlobalSearchPrefix is set." + }, { "id": "model.config.is_valid.elastic_search.enable_autocomplete.app_error", "translation": "{{.EnableIndexing}} setting must be set to true when {{.Autocomplete}} is set to true" @@ -8964,6 +8968,10 @@ "id": "model.config.is_valid.elastic_search.ignored_indexes_dash_prefix.app_error", "translation": "Ignored indexes for purge should not start with dash." }, + { + "id": "model.config.is_valid.elastic_search.incorrect_search_prefix.app_error", + "translation": "GlobalSearchPrefix {{.GlobalSearchPrefix}} should be a prefix of IndexPrefix {{.IndexPrefix}}." + }, { "id": "model.config.is_valid.elastic_search.invalid_backend.app_error", "translation": "Invalid search backend. Must be either elasticsearch or opensearch." diff --git a/server/public/model/config.go b/server/public/model/config.go index 670301aefb4..1d11b1e3224 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -2916,6 +2916,7 @@ type ElasticsearchSettings struct { AggregatePostsAfterDays *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` // telemetry: none PostsAggregatorJobStartTime *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` // telemetry: none IndexPrefix *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` + GlobalSearchPrefix *string `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` LiveIndexingBatchSize *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` BulkIndexingTimeWindowSeconds *int `json:",omitempty"` // telemetry: none BatchSize *int `access:"environment_elasticsearch,write_restrictable,cloud_restrictable"` @@ -3009,6 +3010,10 @@ func (s *ElasticsearchSettings) SetDefaults() { s.IndexPrefix = NewPointer(ElasticsearchSettingsDefaultIndexPrefix) } + if s.GlobalSearchPrefix == nil { + s.GlobalSearchPrefix = NewPointer("") + } + if s.LiveIndexingBatchSize == nil { s.LiveIndexingBatchSize = NewPointer(ElasticsearchSettingsDefaultLiveIndexingBatchSize) } @@ -4427,6 +4432,16 @@ func (s *ElasticsearchSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.invalid_backend.app_error", nil, "", http.StatusBadRequest) } + if *s.GlobalSearchPrefix != "" && *s.IndexPrefix == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.empty_index_prefix.app_error", nil, "", http.StatusBadRequest) + } + + if *s.GlobalSearchPrefix != "" && *s.IndexPrefix != "" { + if !strings.HasPrefix(*s.IndexPrefix, *s.GlobalSearchPrefix) { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.incorrect_search_prefix.app_error", map[string]any{"IndexPrefix": *s.IndexPrefix, "GlobalSearchPrefix": *s.GlobalSearchPrefix}, "", http.StatusBadRequest) + } + } + return nil }