From aa59c28b04fa45f0b02e5149ac0fbcd2b78420b9 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 16 May 2022 13:16:11 +0530 Subject: [PATCH] POC: Cross-team recent search (#20027) Just a quick POC to move fast :P We use a search pointer to keep track of the next row to inesrt to. For every new search we increment the pointer and do modulo 5. This means that the value will always remain between 0-4. And that way, we will always overwrite the oldest entry on every search. And while getting the results, we get all results for that user. The search parameters are json marshalled and stored as a JSON blob. This is because there is no need to search/filter them in the DB. Pending items: Tests obviously. To improve: The client needs to send the channel ids instead of channel names. --- api4/file.go | 26 +++- api4/post.go | 11 +- api4/post_test.go | 24 ++-- api4/user.go | 23 ++++ app/app_iface.go | 5 +- app/file.go | 3 +- app/file_test.go | 12 +- app/opentracing/opentracing_layer.go | 30 +++- app/plugin_api.go | 2 +- app/post.go | 15 +- app/post_test.go | 12 +- .../mysql/000084_recent_searches.down.sql | 1 + .../mysql/000084_recent_searches.up.sql | 7 + .../postgres/000084_recent_searches.down.sql | 1 + .../postgres/000084_recent_searches.up.sql | 7 + i18n/en.json | 4 + model/post.go | 6 + model/search_params.go | 39 +++--- store/opentracinglayer/opentracinglayer.go | 36 +++++ store/retrylayer/retrylayer.go | 42 ++++++ store/sqlstore/channel_store.go | 25 +++- store/sqlstore/file_info_store.go | 24 +++- store/sqlstore/post_store.go | 130 ++++++++++++++++-- store/store.go | 2 + store/storetest/mocks/PostStore.go | 37 +++++ store/timerlayer/timerlayer.go | 32 +++++ 26 files changed, 488 insertions(+), 68 deletions(-) create mode 100644 db/migrations/mysql/000084_recent_searches.down.sql create mode 100644 db/migrations/mysql/000084_recent_searches.up.sql create mode 100644 db/migrations/postgres/000084_recent_searches.down.sql create mode 100644 db/migrations/postgres/000084_recent_searches.up.sql diff --git a/api4/file.go b/api4/file.go index 72e50a0d6f..17b0de4820 100644 --- a/api4/file.go +++ b/api4/file.go @@ -56,13 +56,14 @@ const maxMultipartFormDataBytes = 10 * 1024 // 10Kb func (api *API) InitFile() { api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST") + api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST") api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET") api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET") api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET") api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods("GET") api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods("GET") - api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFiles)).Methods("POST") + api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods("POST") api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods("GET") @@ -734,7 +735,7 @@ func writeFileResponse(filename string, contentType string, contentSize int64, l http.ServeContent(w, r, filename, lastModification, fileReader) } -func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) { +func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) { c.RequireTeamId() if c.Err != nil { return @@ -745,6 +746,16 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) { return } + searchFiles(c, w, r, c.Params.TeamId) +} + +func searchFilesForUser(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.Config().FeatureFlags.CommandPalette { + searchFiles(c, w, r, "") + } +} + +func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) { var params model.SearchParameter jsonErr := json.NewDecoder(r.Body).Decode(¶ms) if jsonErr != nil { @@ -783,9 +794,18 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request) { includeDeletedChannels = *params.IncludeDeletedChannels } + modifier := "" + if params.Modifier != nil { + modifier = *params.Modifier + } + if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages { + c.SetInvalidParam("modifier") + return + } + startTime := time.Now() - results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, c.Params.TeamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) elapsedTime := float64(time.Since(startTime)) / float64(time.Second) metrics := c.App.Metrics() diff --git a/api4/post.go b/api4/post.go index a68ecb4dc8..5a55771e8d 100644 --- a/api4/post.go +++ b/api4/post.go @@ -644,9 +644,18 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId stri includeDeletedChannels = *params.IncludeDeletedChannels } + modifier := "" + if params.Modifier != nil { + modifier = *params.Modifier + } + if modifier != "" && modifier != model.ModifierFiles && modifier != model.ModifierMessages { + c.SetInvalidParam("modifier") + return + } + startTime := time.Now() - results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) elapsedTime := float64(time.Since(startTime)) / float64(time.Second) metrics := c.App.Metrics() diff --git a/api4/post_test.go b/api4/post_test.go index 00c7a6474e..60fceefb4b 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -2458,19 +2458,23 @@ func TestSearchPostsFromUser(t *testing.T) { message = "sgtitlereview\n with return" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) - posts, _, _ := client.SearchPosts(th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false) + posts, _, err := client.SearchPosts(th.BasicTeam.Id, "from: "+th.TeamAdminUser.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.TeamAdminUser.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v", th.BasicUser2.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" sgtitlereview", false) + require.NoError(t, err) require.Lenf(t, posts.Order, 1, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username) message = "hullo" _ = th.CreateMessagePost(message) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" in:"+th.BasicChannel.Name, false) + require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts for search 'from: %v in:", th.BasicUser2.Username, th.BasicChannel.Name) client.Login(user.Email, user.Password) @@ -2478,19 +2482,23 @@ func TestSearchPostsFromUser(t *testing.T) { // wait for the join/leave messages to be created for user3 since they're done asynchronously time.Sleep(100 * time.Millisecond) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v'", th.BasicUser2.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username, false) + require.NoError(t, err) require.Lenf(t, posts.Order, 2, "wrong number of posts for search 'from: %v from: %v'", th.BasicUser2.Username, user.Username) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name, false) + require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") message = "coconut" _ = th.CreateMessagePostWithClient(client, th.BasicChannel2, message) - posts, _, _ = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false) + posts, _, err = client.SearchPosts(th.BasicTeam.Id, "from: "+th.BasicUser2.Username+" from: "+user.Username+" in:"+th.BasicChannel2.Name+" coconut", false) + require.NoError(t, err) require.Len(t, posts.Order, 1, "wrong number of posts") } diff --git a/api4/user.go b/api4/user.go index 2d96912499..3b45822a0b 100644 --- a/api4/user.go +++ b/api4/user.go @@ -92,6 +92,7 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/uploads", api.APISessionRequired(getUploadsForUser)).Methods("GET") api.BaseRoutes.User.Handle("/channel_members", api.APISessionRequired(getChannelMembersForUser)).Methods("GET") + api.BaseRoutes.User.Handle("/recent_searches", api.APISessionRequiredDisableWhenBusy(getRecentSearches)).Methods("GET") api.BaseRoutes.Users.Handle("/invalid_emails", api.APISessionRequired(getUsersWithInvalidEmails)).Methods("GET") @@ -3245,3 +3246,25 @@ func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Reques b, _ := json.Marshal(users) w.Write(b) } + +func getRecentSearches(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) { + c.SetPermissionError(model.PermissionEditOtherUsers) + return + } + + searchParams, err := c.App.GetRecentSearchesForUser(c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if err := json.NewEncoder(w).Encode(searchParams); err != nil { + mlog.Warn("Error while writing response", mlog.Err(err)) + } +} diff --git a/app/app_iface.go b/app/app_iface.go index b518746431..058cd5b640 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -689,6 +689,7 @@ type AppIface interface { GetPublicChannelsByIdsForTeam(teamID string, channelIDs []string) (model.ChannelList, *model.AppError) GetPublicChannelsForTeam(teamID string, offset int, limit int) (model.ChannelList, *model.AppError) GetReactionsForPost(postID string) ([]*model.Reaction, *model.AppError) + GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) GetRecentlyActiveUsersForTeamPage(teamID string, page, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) GetRemoteCluster(remoteClusterId string) (*model.RemoteCluster, *model.AppError) @@ -955,9 +956,9 @@ type AppIface interface { SearchChannelsUserNotIn(teamID string, userID string, term string) (model.ChannelList, *model.AppError) SearchEmoji(name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) SearchEngine() *searchengine.Broker - SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.FileInfoList, *model.AppError) + SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.FileInfoList, *model.AppError) SearchGroupChannels(userID, term string) (model.ChannelList, *model.AppError) - SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) + SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) SearchPrivateTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) SearchPublicTeams(searchOpts *model.TeamSearch) ([]*model.Team, *model.AppError) diff --git a/app/file.go b/app/file.go index 571d6815ec..edabfaed9a 100644 --- a/app/file.go +++ b/app/file.go @@ -1268,7 +1268,7 @@ func populateZipfile(w *zip.Writer, fileDatas []model.FileData) error { return nil } -func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.FileInfoList, *model.AppError) { +func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.FileInfoList, *model.AppError) { paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset) includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels @@ -1279,6 +1279,7 @@ func (a *App) SearchFilesInTeamForUser(c *request.Context, terms string, userId finalParamsList := []*model.SearchParams{} for _, params := range paramsList { + params.Modifier = modifier params.OrTerms = isOrSearch params.IncludeDeletedChannels = includeDeleted // Don't allow users to search for "*" diff --git a/app/file_test.go b/app/file_test.go index c5176ee3cd..628a912e9e 100644 --- a/app/file_test.go +++ b/app/file_test.go @@ -399,7 +399,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { page := 0 - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -420,7 +420,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { page := 1 - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -451,7 +451,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -480,7 +480,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -505,7 +505,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) require.NotNil(t, results) @@ -538,7 +538,7 @@ func TestSearchFilesInTeamForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierFiles) require.Nil(t, err) assert.Equal(t, []string{}, results.Order) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index afb5d1f511..dd5f8d033c 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -8069,6 +8069,28 @@ func (a *OpenTracingAppLayer) GetReactionsForPost(postID string) ([]*model.React return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentSearchesForUser") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0, resultVar1 := a.app.GetRecentSearchesForUser(userID) + + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 +} + func (a *OpenTracingAppLayer) GetRecentlyActiveUsersForTeam(teamID string) (map[string]*model.User, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetRecentlyActiveUsersForTeam") @@ -14175,7 +14197,7 @@ func (a *OpenTracingAppLayer) SearchEngine() *searchengine.Broker { return resultVar0 } -func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int) (*model.FileInfoList, *model.AppError) { +func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.FileInfoList, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchFilesInTeamForUser") @@ -14187,7 +14209,7 @@ func (a *OpenTracingAppLayer) SearchFilesInTeamForUser(c *request.Context, terms }() defer span.Finish() - resultVar0, resultVar1 := a.app.SearchFilesInTeamForUser(c, terms, userId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + resultVar0, resultVar1 := a.app.SearchFilesInTeamForUser(c, terms, userId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -14219,7 +14241,7 @@ func (a *OpenTracingAppLayer) SearchGroupChannels(userID string, term string) (m return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int) (*model.PostSearchResults, *model.AppError) { +func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page int, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SearchPostsForUser") @@ -14231,7 +14253,7 @@ func (a *OpenTracingAppLayer) SearchPostsForUser(c *request.Context, terms strin }() defer span.Finish() - resultVar0, resultVar1 := a.app.SearchPostsForUser(c, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + resultVar0, resultVar1 := a.app.SearchPostsForUser(c, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, modifier) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/app/plugin_api.go b/app/plugin_api.go index 4d62775894..0cb627cb70 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -548,7 +548,7 @@ func (api *PluginAPI) SearchPostsInTeamForUser(teamID string, userID string, sea includeDeletedChannels = *searchParams.IncludeDeletedChannels } - return api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage) + return api.app.SearchPostsForUser(api.ctx, terms, userID, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage, model.ModifierMessages) } func (api *PluginAPI) AddChannelMember(channelID, userID string) (*model.ChannelMember, *model.AppError) { diff --git a/app/post.go b/app/post.go index 3aedc95953..56d1906d68 100644 --- a/app/post.go +++ b/app/post.go @@ -1305,7 +1305,7 @@ func (a *App) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) }) } -func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) { +func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int, modifier string) (*model.PostSearchResults, *model.AppError) { var postSearchResults *model.PostSearchResults paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset) includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels @@ -1317,10 +1317,14 @@ func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string finalParamsList := []*model.SearchParams{} for _, params := range paramsList { + params.Modifier = modifier params.OrTerms = isOrSearch params.IncludeDeletedChannels = includeDeleted // Don't allow users to search for "*" if params.Terms != "*" { + // TODO: we have to send channel ids + // from the front-end. Otherwise it's not possible to distinguish + // from just the channel name at a cross-team level. // Convert channel names to channel IDs params.InChannels = a.convertChannelNamesToChannelIds(c, params.InChannels, userID, teamID, includeDeletedChannels) params.ExcludedChannels = a.convertChannelNamesToChannelIds(c, params.ExcludedChannels, userID, teamID, includeDeletedChannels) @@ -1352,6 +1356,15 @@ func (a *App) SearchPostsForUser(c *request.Context, terms string, userID string return postSearchResults, nil } +func (a *App) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, *model.AppError) { + searchParams, nErr := a.Srv().Store.Post().GetRecentSearchesForUser(userID) + if nErr != nil { + return nil, model.NewAppError("GetRecentSearchesForUser", "app.recent_searches.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + + return searchParams, nil +} + func (a *App) GetFileInfosForPostWithMigration(postID string) ([]*model.FileInfo, *model.AppError) { pchan := make(chan store.StoreResult, 1) diff --git a/app/post_test.go b/app/post_test.go index b57731865b..5975c92d61 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -1423,7 +1423,7 @@ func TestSearchPostsForUser(t *testing.T) { page := 0 - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{ @@ -1443,7 +1443,7 @@ func TestSearchPostsForUser(t *testing.T) { page := 1 - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{}, results.Order) @@ -1473,7 +1473,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, resultsPage, results.Order) @@ -1501,7 +1501,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, resultsPage, results.Order) @@ -1525,7 +1525,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{ @@ -1557,7 +1557,7 @@ func TestSearchPostsForUser(t *testing.T) { th.App.Srv().SearchEngine.ElasticsearchEngine = nil }() - results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage) + results, err := th.App.SearchPostsForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage, model.ModifierMessages) assert.Nil(t, err) assert.Equal(t, []string{}, results.Order) diff --git a/db/migrations/mysql/000084_recent_searches.down.sql b/db/migrations/mysql/000084_recent_searches.down.sql new file mode 100644 index 0000000000..c2e20271c6 --- /dev/null +++ b/db/migrations/mysql/000084_recent_searches.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS RecentSearches; \ No newline at end of file diff --git a/db/migrations/mysql/000084_recent_searches.up.sql b/db/migrations/mysql/000084_recent_searches.up.sql new file mode 100644 index 0000000000..33986fa149 --- /dev/null +++ b/db/migrations/mysql/000084_recent_searches.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS RecentSearches ( + UserId CHAR(26), + SearchPointer int, + Query json, + CreateAt bigint NOT NULL, + PRIMARY KEY (UserId, SearchPointer) +); \ No newline at end of file diff --git a/db/migrations/postgres/000084_recent_searches.down.sql b/db/migrations/postgres/000084_recent_searches.down.sql new file mode 100644 index 0000000000..30d2c222e9 --- /dev/null +++ b/db/migrations/postgres/000084_recent_searches.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS recentsearches; \ No newline at end of file diff --git a/db/migrations/postgres/000084_recent_searches.up.sql b/db/migrations/postgres/000084_recent_searches.up.sql new file mode 100644 index 0000000000..5e9930753a --- /dev/null +++ b/db/migrations/postgres/000084_recent_searches.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS recentsearches ( + userid CHAR(26), + searchpointer int, + query jsonb, + createat bigint NOT NULL, + PRIMARY KEY (userid, searchpointer) +); \ No newline at end of file diff --git a/i18n/en.json b/i18n/en.json index 0e1c390d97..c68a18a9a5 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5939,6 +5939,10 @@ "id": "app.reaction.save.save.app_error", "translation": "Unable to save reaction." }, + { + "id": "app.recent_searches.app_error", + "translation": "Error fetching recent searches" + }, { "id": "app.recover.delete.app_error", "translation": "Unable to delete token." diff --git a/model/post.go b/model/post.go index 039f761eea..f7ffcb12c4 100644 --- a/model/post.go +++ b/model/post.go @@ -71,6 +71,11 @@ const ( PostPropsPreviewedPost = "previewed_post" ) +const ( + ModifierMessages string = "messages" + ModifierFiles string = "files" +) + type Post struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` @@ -127,6 +132,7 @@ type SearchParameter struct { Page *int `json:"page"` PerPage *int `json:"per_page"` IncludeDeletedChannels *bool `json:"include_deleted_channels"` + Modifier *string `json:"modifier"` // whether it's messages or file } type AnalyticsPostCountsOptions struct { diff --git a/model/search_params.go b/model/search_params.go index 41a2db2aba..c902ca2f43 100644 --- a/model/search_params.go +++ b/model/search_params.go @@ -14,26 +14,27 @@ var searchTermPuncStart = regexp.MustCompile(`^[^\pL\d\s#"]+`) var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`) type SearchParams struct { - Terms string - ExcludedTerms string - IsHashtag bool - InChannels []string - ExcludedChannels []string - FromUsers []string - ExcludedUsers []string - AfterDate string - ExcludedAfterDate string - BeforeDate string - ExcludedBeforeDate string - Extensions []string - ExcludedExtensions []string - OnDate string - ExcludedDate string - OrTerms bool - IncludeDeletedChannels bool - TimeZoneOffset int + Terms string `json:"terms,omitempty"` + ExcludedTerms string `json:"excluded_terms,omitempty"` + IsHashtag bool `json:"ishashtag,omitempty"` + InChannels []string `json:"in_channels,omitempty"` + ExcludedChannels []string `json:"excluded_channels,omitempty"` + FromUsers []string `json:"from_users,omitempty"` + ExcludedUsers []string `json:"excluded_users,omitempty"` + AfterDate string `json:"after_date,omitempty"` + ExcludedAfterDate string `json:"excluded_after_date,omitempty"` + BeforeDate string `json:"before_date,omitempty"` + ExcludedBeforeDate string `json:"excluded_before_date,omitempty"` + Extensions []string `json:"extensions,omitempty"` + ExcludedExtensions []string `json:"excluded_extensions,omitempty"` + OnDate string `json:"on_date,omitempty"` + ExcludedDate string `json:"excluded_date,omitempty"` + OrTerms bool `json:"or_terms,omitempty"` + IncludeDeletedChannels bool `json:"include_deleted_channels,omitempty"` + TimeZoneOffset int `json:"timezone_offset,omitempty"` // True if this search doesn't originate from a "current user". - SearchWithoutUserId bool + SearchWithoutUserId bool `json:"search_without_userid,omitempty"` + Modifier string `json:"modifier"` } // Returns the epoch timestamp of the start of the day specified by SearchParams.AfterDate diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index a88d15a711..4b7a0e6eb2 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -5862,6 +5862,24 @@ func (s *OpenTracingLayerPostStore) GetPostsSinceForSync(options model.GetPostsS return result, resultVar1, err } +func (s *OpenTracingLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRecentSearchesForUser") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.PostStore.GetRecentSearchesForUser(userID) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.GetRepliesForExport") @@ -5929,6 +5947,24 @@ func (s *OpenTracingLayerPostStore) InvalidateLastPostTimeCache(channelID string } +func (s *OpenTracingLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.LogRecentSearch") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.Overwrite") diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 61bfa3392e..09917f12ba 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -6637,6 +6637,27 @@ func (s *RetryLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceFo } +func (s *RetryLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + + tries := 0 + for { + result, err := s.PostStore.GetRecentSearchesForUser(userID) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { tries := 0 @@ -6706,6 +6727,27 @@ func (s *RetryLayerPostStore) InvalidateLastPostTimeCache(channelID string) { } +func (s *RetryLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + + tries := 0 + for { + err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) { tries := 0 diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index b501749138..440dbcb79f 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1522,11 +1522,19 @@ func (s SqlChannelStore) GetByNameIncludeDeleted(teamId string, name string, all } func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bool, allowFromCache bool) (*model.Channel, error) { - var query string - if includeDeleted { - query = "SELECT * FROM Channels WHERE (TeamId = ? OR TeamId = '') AND Name = ?" - } else { - query = "SELECT * FROM Channels WHERE (TeamId = ? OR TeamId = '') AND Name = ? AND DeleteAt = 0" + query := s.getQueryBuilder(). + Select("*"). + From("Channels"). + Where(sq.Eq{"Name": name}) + + if !includeDeleted { + query = query.Where(sq.Eq{"DeleteAt": 0}) + } + if teamId != "" { + query = query.Where(sq.Or{ + sq.Eq{"TeamId": teamId}, + sq.Eq{"TeamId": ""}, + }) } channel := model.Channel{} @@ -1543,7 +1551,12 @@ func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bo } } - if err := s.GetReplicaX().Get(&channel, query, teamId, name); err != nil { + queryStr, args, err := query.ToSql() + if err != nil { + return nil, errors.Wrapf(err, "getByName_tosql") + } + + if err := s.GetReplicaX().Get(&channel, queryStr, args...); err != nil { if err == sql.ErrNoRows { return nil, store.NewErrNotFound("Channel", fmt.Sprintf("TeamId=%s&Name=%s", teamId, name)) } diff --git a/store/sqlstore/file_info_store.go b/store/sqlstore/file_info_store.go index ae14ad0a10..ceb24bd39b 100644 --- a/store/sqlstore/file_info_store.go +++ b/store/sqlstore/file_info_store.go @@ -5,6 +5,7 @@ package sqlstore import ( "database/sql" + "encoding/json" "fmt" "regexp" "strconv" @@ -507,12 +508,33 @@ func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, team LeftJoin("Posts as P ON FileInfo.PostId=P.Id"). LeftJoin("Channels as C ON C.Id=P.ChannelId"). LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId"). - Where(sq.Or{sq.Eq{"C.TeamId": teamId}, sq.Eq{"C.TeamId": ""}}). Where(sq.Eq{"FileInfo.DeleteAt": 0}). OrderBy("FileInfo.CreateAt DESC"). Limit(100) + if teamId != "" { + query = query.Where(sq.Or{ + sq.Eq{"C.TeamId": teamId}, + sq.Eq{"C.TeamId": ""}, + }) + } + + now := model.GetMillis() for _, params := range paramsList { + if params.Modifier == model.ModifierFiles { + // Deliberately keeping non-alphanumeric characters to + // prevent surprises in UI. + buf, err := json.Marshal(params) + if err != nil { + return nil, err + } + + err = fs.stores.post.LogRecentSearch(userId, buf, now) + if err != nil { + return nil, err + } + } + params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ") if !params.IncludeDeletedChannels { diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index dfa4b62d1e..9ae4b158a1 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -6,9 +6,11 @@ package sqlstore import ( "context" "database/sql" + "encoding/json" "fmt" "reflect" "regexp" + "strconv" "strings" "sync" @@ -1802,11 +1804,13 @@ func (s *SqlPostStore) buildSearchPostFilterClause(teamID string, fromUsers []st } // Sub-query builder. - sb := s.getSubQueryBuilder().Select("Id").From("Users, TeamMembers").Where( - sq.And{ - sq.Eq{"TeamMembers.TeamId": teamID}, - sq.Expr("Users.Id = TeamMembers.UserId"), - }) + sb := s.getSubQueryBuilder(). + Select("Id"). + From("Users, TeamMembers"). + Where(sq.Expr("Users.Id = TeamMembers.UserId")) + if teamID != "" { + sb = sb.Where(sq.Eq{"TeamMembers.TeamId": teamID}) + } sb = s.buildSearchUserFilterClause(fromUsers, false, userByUsername, sb) sb = s.buildSearchUserFilterClause(excludedUsers, true, userByUsername, sb) subQuery, subQueryArgs, err := sb.ToSql() @@ -2522,7 +2526,7 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str } //nolint:unparam -func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userId, teamId string, page, perPage int) (*model.PostSearchResults, error) { +func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamId string, page, perPage int) (*model.PostSearchResults, error) { // Since we don't support paging for DB search, we just return nothing for later pages if page > 0 { return model.MakePostSearchResults(model.NewPostList(), nil), nil @@ -2532,11 +2536,22 @@ func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, user return nil, err } - var wg sync.WaitGroup - + now := model.GetMillis() pchan := make(chan store.StoreResult, len(paramsList)) + var wg sync.WaitGroup for _, params := range paramsList { + // Deliberately keeping non-alphanumeric characters to + // prevent surprises in UI. + buf, err := json.Marshal(params) + if err != nil { + return nil, err + } + err = s.LogRecentSearch(userID, buf, now) + if err != nil { + return nil, err + } + // remove any unquoted term that contains only non-alphanumeric chars // ex: abcd "**" && abc >> abcd "**" abc params.Terms = removeNonAlphaNumericUnquotedTerms(params.Terms, " ") @@ -2545,7 +2560,7 @@ func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, user go func(params *model.SearchParams) { defer wg.Done() - postList, err := s.search(teamId, userId, params, false, false) + postList, err := s.search(teamId, userID, params, false, false) pchan <- store.StoreResult{Data: postList, NErr: err} }(params) } @@ -2568,6 +2583,103 @@ func (s *SqlPostStore) SearchPostsForUser(paramsList []*model.SearchParams, user return model.MakePostSearchResults(posts, nil), nil } +const lastSearchesLimit = 5 + +func (s *SqlPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + transaction, err := s.GetMasterX().Beginx() + if err != nil { + return errors.Wrap(err, "begin_transaction") + } + + defer finalizeTransactionX(transaction) + + var lastSearchPointer int + var queryStr string + // get search_pointer + // We coalesce to -1 because we want to start from 0 + if s.DriverName() == model.DatabaseDriverPostgres { + queryStr = `SELECT COALESCE((props->>'last_search_pointer')::integer, -1) + FROM Users + WHERE Id=?` + } else { + queryStr = `SELECT COALESCE(CAST(JSON_EXTRACT(Props, '$.last_search_pointer') as unsigned), -1) + FROM Users + WHERE Id=?` + } + err = transaction.Get(&lastSearchPointer, queryStr, userID) + if err != nil { + return errors.Wrapf(err, "failed to find last_search_pointer for user=%s", userID) + } + + // (ptr+1)%lastSearchesLimit + lastSearchPointer = (lastSearchPointer + 1) % lastSearchesLimit + + if s.IsBinaryParamEnabled() { + searchQuery = AppendBinaryFlag(searchQuery) + } + + // insert at pointer + query := s.getQueryBuilder(). + Insert("RecentSearches"). + Columns("UserId", "SearchPointer", "Query", "CreateAt"). + Values(userID, lastSearchPointer, searchQuery, createAt) + + if s.DriverName() == model.DatabaseDriverPostgres { + query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, searchpointer) DO UPDATE SET Query = ?, CreateAt = ?", searchQuery, createAt)) + } else { + query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Query = ?, CreateAt = ?", searchQuery, createAt)) + } + + queryString, args, err := query.ToSql() + if err != nil { + return errors.Wrap(err, "log_recent_search_tosql") + } + + if _, err2 := transaction.Exec(queryString, args...); err2 != nil { + return errors.Wrapf(err2, "failed to upsert recent_search for user=%s", userID) + } + + // write ptr on users prop + if s.DriverName() == model.DatabaseDriverPostgres { + _, err = transaction.Exec(`UPDATE Users + SET Props = jsonb_set(Props, $1, $2) + WHERE Id = $3`, jsonKeyPath("last_search_pointer"), jsonStringVal(strconv.Itoa(lastSearchPointer)), userID) + } else { + _, err = transaction.Exec(`UPDATE Users + SET Props = JSON_SET(Props, ?, ?) + WHERE Id = ?`, "$.last_search_pointer", strconv.Itoa(lastSearchPointer), userID) + } + if err != nil { + return errors.Wrapf(err, "failed to update last_search_pointer for user=%s", userID) + } + + if err2 := transaction.Commit(); err2 != nil { + return errors.Wrap(err2, "commit_transaction") + } + + return nil +} + +func (s *SqlPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + params := [][]byte{} + err := s.GetReplicaX().Select(¶ms, `SELECT query + FROM RecentSearches + WHERE UserId=? + ORDER BY CreateAt DESC`, userID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get recent searches for user=%s", userID) + } + + res := make([]*model.SearchParams, len(params)) + for i, param := range params { + err = json.Unmarshal(param, &res[i]) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal recent search query for user=%s", userID) + } + } + return res, nil +} + func (s *SqlPostStore) GetOldestEntityCreationTime() (int64, error) { query := s.getQueryBuilder().Select("MIN(min_createat) min_createat"). Suffix(`FROM ( diff --git a/store/store.go b/store/store.go index f0a9da8771..7330f5fa20 100644 --- a/store/store.go +++ b/store/store.go @@ -366,6 +366,8 @@ type PostStore interface { GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) GetDirectPostParentsForExportAfter(limit int, afterID string) ([]*model.DirectPostForExport, error) SearchPostsForUser(paramsList []*model.SearchParams, userID, teamID string, page, perPage int) (*model.PostSearchResults, error) + GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) + LogRecentSearch(userID string, searchQuery []byte, createAt int64) error GetOldestEntityCreationTime() (int64, error) HasAutoResponsePostByUserSince(options model.GetPostsSinceOptions, userId string) (bool, error) GetPostsSinceForSync(options model.GetPostsSinceForSyncOptions, cursor model.GetPostsSinceForSyncCursor, limit int) ([]*model.Post, model.GetPostsSinceForSyncCursor, error) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 016e986123..21ca52de92 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -610,6 +610,29 @@ func (_m *PostStore) GetPostsSinceForSync(options model.GetPostsSinceForSyncOpti return r0, r1, r2 } +// GetRecentSearchesForUser provides a mock function with given fields: userID +func (_m *PostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + ret := _m.Called(userID) + + var r0 []*model.SearchParams + if rf, ok := ret.Get(0).(func(string) []*model.SearchParams); ok { + r0 = rf(userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.SearchParams) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRepliesForExport provides a mock function with given fields: parentID func (_m *PostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { ret := _m.Called(parentID) @@ -682,6 +705,20 @@ func (_m *PostStore) InvalidateLastPostTimeCache(channelID string) { _m.Called(channelID) } +// LogRecentSearch provides a mock function with given fields: userID, searchQuery, createAt +func (_m *PostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + ret := _m.Called(userID, searchQuery, createAt) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []byte, int64) error); ok { + r0 = rf(userID, searchQuery, createAt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Overwrite provides a mock function with given fields: post func (_m *PostStore) Overwrite(post *model.Post) (*model.Post, error) { ret := _m.Called(post) diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index d0d021e4f2..22caf13918 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -5306,6 +5306,22 @@ func (s *TimerLayerPostStore) GetPostsSinceForSync(options model.GetPostsSinceFo return result, resultVar1, err } +func (s *TimerLayerPostStore) GetRecentSearchesForUser(userID string) ([]*model.SearchParams, error) { + start := timemodule.Now() + + result, err := s.PostStore.GetRecentSearchesForUser(userID) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostStore.GetRecentSearchesForUser", success, elapsed) + } + return result, err +} + func (s *TimerLayerPostStore) GetRepliesForExport(parentID string) ([]*model.ReplyForExport, error) { start := timemodule.Now() @@ -5369,6 +5385,22 @@ func (s *TimerLayerPostStore) InvalidateLastPostTimeCache(channelID string) { } } +func (s *TimerLayerPostStore) LogRecentSearch(userID string, searchQuery []byte, createAt int64) error { + start := timemodule.Now() + + err := s.PostStore.LogRecentSearch(userID, searchQuery, createAt) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("PostStore.LogRecentSearch", success, elapsed) + } + return err +} + func (s *TimerLayerPostStore) Overwrite(post *model.Post) (*model.Post, error) { start := timemodule.Now()