diff options
author | Joram Wilander <jwawilander@gmail.com> | 2016-11-24 09:35:09 -0500 |
---|---|---|
committer | Harrison Healey <harrisonmhealey@gmail.com> | 2016-11-24 09:35:09 -0500 |
commit | 981ea33b8e10456bc279f36235c814305d01b243 (patch) | |
tree | 00fb6119d9ef16f60d4c0dbdaad1bd6dfbc347ed /store | |
parent | c96ecae6da31aceabf29586cde872876b81d11d9 (diff) | |
download | chat-981ea33b8e10456bc279f36235c814305d01b243.tar.gz chat-981ea33b8e10456bc279f36235c814305d01b243.tar.bz2 chat-981ea33b8e10456bc279f36235c814305d01b243.zip |
PLT-4403 Add server-based channel autocomplete, search and paging (#4585)
* Add more channel paging API
* Add channel paging support to client
* Add DB channel search functions
* Add API for searching more channels
* Add more channel search functionality to client
* Add API for autocompleting channels
* Add channel autocomplete functionality to the client
* Move to be deprecated APIs to their own file
* Final clean-up
* Fixes related to feedback
* Localization changes
* Add unit as suffix to timeout constants
Diffstat (limited to 'store')
-rw-r--r-- | store/sql_channel_store.go | 119 | ||||
-rw-r--r-- | store/sql_channel_store_test.go | 214 | ||||
-rw-r--r-- | store/sql_user_store.go | 4 | ||||
-rw-r--r-- | store/store.go | 4 |
4 files changed, 332 insertions, 9 deletions
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index 207484532..4c3eff6e2 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -5,6 +5,8 @@ package store import ( "database/sql" + "fmt" + "strings" l4g "github.com/alecthomas/log4go" "github.com/go-gorp/gorp" @@ -65,6 +67,8 @@ func (s SqlChannelStore) CreateIndexesIfNotExists() { s.CreateIndexIfNotExists("idx_channelmembers_channel_id", "ChannelMembers", "ChannelId") s.CreateIndexIfNotExists("idx_channelmembers_user_id", "ChannelMembers", "UserId") + + s.CreateFullTextIndexIfNotExists("idx_channels_txt", "Channels", "Name, DisplayName") } func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel { @@ -392,7 +396,7 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string) StoreChannel return storeChannel } -func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChannel { +func (s SqlChannelStore) GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel { storeChannel := make(StoreChannel, 1) go func() { @@ -418,8 +422,10 @@ func (s SqlChannelStore) GetMoreChannels(teamId string, userId string) StoreChan AND TeamId = :TeamId2 AND UserId = :UserId AND DeleteAt = 0) - ORDER BY DisplayName`, - map[string]interface{}{"TeamId1": teamId, "TeamId2": teamId, "UserId": userId}) + ORDER BY DisplayName + LIMIT :Limit + OFFSET :Offset`, + map[string]interface{}{"TeamId1": teamId, "TeamId2": teamId, "UserId": userId, "Limit": limit, "Offset": offset}) if err != nil { result.Err = model.NewLocAppError("SqlChannelStore.GetMoreChannels", "store.sql_channel.get_more_channels.get.app_error", nil, "teamId="+teamId+", userId="+userId+", err="+err.Error()) @@ -1104,3 +1110,110 @@ func (s SqlChannelStore) GetMembersForUser(teamId string, userId string) StoreCh return storeChannel } + +func (s SqlChannelStore) SearchInTeam(teamId string, term string) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + searchQuery := ` + SELECT + * + FROM + Channels + WHERE + TeamId = :TeamId + AND Type = 'O' + AND DeleteAt = 0 + SEARCH_CLAUSE + ORDER BY DisplayName + LIMIT 100` + + storeChannel <- s.performSearch(searchQuery, term, map[string]interface{}{"TeamId": teamId}) + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) SearchMore(userId string, teamId string, term string) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + searchQuery := ` + SELECT + * + FROM + Channels + WHERE + TeamId = :TeamId + AND Type = 'O' + AND DeleteAt = 0 + AND Id NOT IN (SELECT + Channels.Id + FROM + Channels, + ChannelMembers + WHERE + Id = ChannelId + AND TeamId = :TeamId + AND UserId = :UserId + AND DeleteAt = 0) + SEARCH_CLAUSE + ORDER BY DisplayName + LIMIT 100` + + storeChannel <- s.performSearch(searchQuery, term, map[string]interface{}{"TeamId": teamId, "UserId": userId}) + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) performSearch(searchQuery string, term string, parameters map[string]interface{}) StoreResult { + result := StoreResult{} + + // these chars have special meaning and can be treated as spaces + for _, c := range specialSearchChar { + term = strings.Replace(term, c, " ", -1) + } + + if term == "" { + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) + } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { + splitTerm := strings.Fields(term) + for i, t := range strings.Fields(term) { + if i == len(splitTerm)-1 { + splitTerm[i] = t + ":*" + } else { + splitTerm[i] = t + ":* &" + } + } + + term = strings.Join(splitTerm, " ") + + searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery('simple', :Term)", "Name || ' ' || DisplayName") + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) + } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { + splitTerm := strings.Fields(term) + for i, t := range strings.Fields(term) { + splitTerm[i] = "+" + t + "*" + } + + term = strings.Join(splitTerm, " ") + + searchClause := fmt.Sprintf("AND MATCH(%s) AGAINST (:Term IN BOOLEAN MODE)", "Name, DisplayName") + searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) + } + + var channels model.ChannelList + + parameters["Term"] = term + + if _, err := s.GetReplica().Select(&channels, searchQuery, parameters); err != nil { + result.Err = model.NewLocAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error()) + } else { + result.Data = &channels + } + + return result +} diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index 6776a438b..9b77639b0 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -309,7 +309,7 @@ func TestChannelStoreDelete(t *testing.T) { t.Fatal("invalid number of channels") } - cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId) + cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100) list = cresult.Data.(*model.ChannelList) if len(*list) != 1 { @@ -621,7 +621,10 @@ func TestChannelStoreGetMoreChannels(t *testing.T) { o5.Type = model.CHANNEL_PRIVATE Must(store.Channel().Save(&o5)) - cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId) + cresult := <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100) + if cresult.Err != nil { + t.Fatal(cresult.Err) + } list := cresult.Data.(*model.ChannelList) if len(*list) != 1 { @@ -632,10 +635,38 @@ func TestChannelStoreGetMoreChannels(t *testing.T) { t.Fatal("missing channel") } + o6 := model.Channel{} + o6.TeamId = o1.TeamId + o6.DisplayName = "ChannelA" + o6.Name = "a" + model.NewId() + "b" + o6.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o6)) + + cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 100) + list = cresult.Data.(*model.ChannelList) + + if len(*list) != 2 { + t.Fatal("wrong list length") + } + + cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 0, 1) + list = cresult.Data.(*model.ChannelList) + + if len(*list) != 1 { + t.Fatal("wrong list length") + } + + cresult = <-store.Channel().GetMoreChannels(o1.TeamId, m1.UserId, 1, 1) + list = cresult.Data.(*model.ChannelList) + + if len(*list) != 1 { + t.Fatal("wrong list length") + } + if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil { t.Fatal(r1.Err) } else { - if r1.Data.(int64) != 2 { + if r1.Data.(int64) != 3 { t.Log(r1.Data) t.Fatal("wrong value") } @@ -1055,3 +1086,180 @@ func TestUpdateExtrasByUser(t *testing.T) { t.Fatal("failed to update extras by user: %v", result.Err) } } + +func TestChannelStoreSearchMore(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "ChannelA" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o1)) + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Channel2" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o2)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + Must(store.Channel().SaveMember(&m1)) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + Must(store.Channel().SaveMember(&m2)) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = model.NewId() + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + Must(store.Channel().SaveMember(&m3)) + + o3 := model.Channel{} + o3.TeamId = o1.TeamId + o3.DisplayName = "ChannelA" + o3.Name = "a" + model.NewId() + "b" + o3.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o3)) + + o4 := model.Channel{} + o4.TeamId = o1.TeamId + o4.DisplayName = "ChannelB" + o4.Name = "a" + model.NewId() + "b" + o4.Type = model.CHANNEL_PRIVATE + Must(store.Channel().Save(&o4)) + + o5 := model.Channel{} + o5.TeamId = o1.TeamId + o5.DisplayName = "ChannelC" + o5.Name = "a" + model.NewId() + "b" + o5.Type = model.CHANNEL_PRIVATE + Must(store.Channel().Save(&o5)) + + if result := <-store.Channel().SearchMore(m1.UserId, o1.TeamId, "ChannelA"); result.Err != nil { + t.Fatal(result.Err) + } else { + channels := result.Data.(*model.ChannelList) + if len(*channels) == 0 { + t.Fatal("should not be empty") + } + + if (*channels)[0].Name != o3.Name { + t.Fatal("wrong channel returned") + } + } + + if result := <-store.Channel().SearchMore(m1.UserId, o1.TeamId, o4.Name); result.Err != nil { + t.Fatal(result.Err) + } else { + channels := result.Data.(*model.ChannelList) + if len(*channels) != 0 { + t.Fatal("should be empty") + } + } + + if result := <-store.Channel().SearchMore(m1.UserId, o1.TeamId, o3.Name); result.Err != nil { + t.Fatal(result.Err) + } else { + channels := result.Data.(*model.ChannelList) + if len(*channels) == 0 { + t.Fatal("should not be empty") + } + + if (*channels)[0].Name != o3.Name { + t.Fatal("wrong channel returned") + } + } + +} + +func TestChannelStoreSearchInTeam(t *testing.T) { + Setup() + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "ChannelA" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o1)) + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Channel2" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o2)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = model.NewId() + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + Must(store.Channel().SaveMember(&m1)) + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = model.NewId() + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + Must(store.Channel().SaveMember(&m2)) + + m3 := model.ChannelMember{} + m3.ChannelId = o2.Id + m3.UserId = model.NewId() + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + Must(store.Channel().SaveMember(&m3)) + + o3 := model.Channel{} + o3.TeamId = o1.TeamId + o3.DisplayName = "ChannelA" + o3.Name = "a" + model.NewId() + "b" + o3.Type = model.CHANNEL_OPEN + Must(store.Channel().Save(&o3)) + + o4 := model.Channel{} + o4.TeamId = o1.TeamId + o4.DisplayName = "ChannelB" + o4.Name = "a" + model.NewId() + "b" + o4.Type = model.CHANNEL_PRIVATE + Must(store.Channel().Save(&o4)) + + o5 := model.Channel{} + o5.TeamId = o1.TeamId + o5.DisplayName = "ChannelC" + o5.Name = "a" + model.NewId() + "b" + o5.Type = model.CHANNEL_PRIVATE + Must(store.Channel().Save(&o5)) + + if result := <-store.Channel().SearchInTeam(o1.TeamId, "ChannelA"); result.Err != nil { + t.Fatal(result.Err) + } else { + channels := result.Data.(*model.ChannelList) + if len(*channels) != 2 { + t.Fatal("wrong length") + } + } + + if result := <-store.Channel().SearchInTeam(o1.TeamId, ""); result.Err != nil { + t.Fatal(result.Err) + } else { + channels := result.Data.(*model.ChannelList) + if len(*channels) == 0 { + t.Fatal("should not be empty") + } + } + + if result := <-store.Channel().SearchInTeam(o1.TeamId, "blargh"); result.Err != nil { + t.Fatal(result.Err) + } else { + channels := result.Data.(*model.ChannelList) + if len(*channels) != 0 { + t.Fatal("should be empty") + } + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 1a38e89e8..b09b479a9 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -1120,7 +1120,7 @@ func (us SqlUserStore) Search(teamId string, term string, options map[string]boo SEARCH_CLAUSE INACTIVE_CLAUSE ORDER BY Username ASC - LIMIT 50` + LIMIT 100` } else { searchQuery = ` SELECT @@ -1264,7 +1264,7 @@ func (us SqlUserStore) performSearch(searchQuery string, term string, options ma term = strings.Join(splitTerm, " ") searchType = convertMySQLFullTextColumnsToPostgres(searchType) - searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery(:Term)", searchType) + searchClause := fmt.Sprintf("AND (%s) @@ to_tsquery('simple', :Term)", searchType) searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) } else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL { splitTerm := strings.Fields(term) diff --git a/store/store.go b/store/store.go index 94c8416bd..ae938a797 100644 --- a/store/store.go +++ b/store/store.go @@ -90,7 +90,7 @@ type ChannelStore interface { PermanentDeleteByTeam(teamId string) StoreChannel GetByName(team_id string, domain string) StoreChannel GetChannels(teamId string, userId string) StoreChannel - GetMoreChannels(teamId string, userId string) StoreChannel + GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel GetChannelCounts(teamId string, userId string) StoreChannel GetTeamChannels(teamId string) StoreChannel GetAll(teamId string) StoreChannel @@ -113,6 +113,8 @@ type ChannelStore interface { AnalyticsTypeCount(teamId string, channelType string) StoreChannel ExtraUpdateByUser(userId string, time int64) StoreChannel GetMembersForUser(teamId string, userId string) StoreChannel + SearchInTeam(teamId string, term string) StoreChannel + SearchMore(userId string, teamId string, term string) StoreChannel } type PostStore interface { |