MM-63298: [AI assisted] Elasticsearch add a global search prefix (#30417)

This PR adds functionality to search by a global search prefix.
This allows Mattermost to be used across multiple data centers
with multiple Elasticsearch instances synchronized using
cross-cluster replication.

While here, we also add tests cases to cover for some missing
search interface methods.

For now, no system console setting is exposed. Because IndexPrefix
is also not exposed. It can be added later if a need arises.

https://mattermost.atlassian.net/browse/MM-63298

```release-note
A new config setting ElasticsearchSettings.GlobalSearchPrefix is added
which can be used to search across multiple indices having a common prefix.
This is useful in a scenario with multiple Elasticsearch instances, where
multiple instances are writing to different indices with different prefixes
using the ElasticsearchSettings.IndexPrefix setting.
```

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Agniva De Sarker 2025-04-04 11:42:12 +05:30 committed by GitHub
parent 7250095f86
commit 09488558a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 488 additions and 23 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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"))

View file

@ -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,
}).

View file

@ -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),

View file

@ -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."

View file

@ -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
}