Added from:, in:, and channel: search modifiers

Этот коммит содержится в:
hmhealey
2015-10-17 14:37:51 -04:00
родитель 754f1721fe
Коммит 06fd374c19
8 изменённых файлов: 419 добавлений и 67 удалений

Просмотреть файл

@@ -680,16 +680,16 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
hashtagTerms, plainTerms := model.ParseHashtags(terms)
plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms)
var hchan store.StoreChannel
if len(hashtagTerms) != 0 {
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagTerms, true)
if hashtagSearchParams != nil {
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams)
}
var pchan store.StoreChannel
if len(plainTerms) != 0 {
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, terms, false)
if plainSearchParams != nil {
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams)
}
mainList := &model.PostList{}

Просмотреть файл

@@ -406,6 +406,128 @@ func TestSearchHashtagPosts(t *testing.T) {
}
}
func TestSearchPostsInChannel(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
Client.LoginByEmail(team.Name, user1.Email, "pwd")
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"}
post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
if result := Client.Must(Client.SearchPosts("channel:")).Data.(*model.PostList); len(result.Order) != 0 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("in:")).Data.(*model.PostList); len(result.Order) != 0 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("ChAnNeL: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview")).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview in:")).Data.(*model.PostList); len(result.Order) != 2 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview channel:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview in: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
}
func TestSearchPostsFromUser(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user1.Id))
Client.LoginByEmail(team.Name, user1.Email, "pwd")
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
post1 := &model.Post{ChannelId: channel1.Id, Message: "sgtitlereview with space"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
Client.LoginByEmail(team.Name, user2.Email, "pwd")
Client.Must(Client.JoinChannel(channel1.Id))
Client.Must(Client.JoinChannel(channel2.Id))
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
// note that this includes the "User2 has joined the channel" system messages
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " sgtitlereview")).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " in:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
t.Fatalf("wrong number of posts returned %v", len(result.Order))
}
}
func TestGetPostsCache(t *testing.T) {
Setup()

130
model/search_params.go Обычный файл
Просмотреть файл

@@ -0,0 +1,130 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
)
type SearchParams struct {
Terms string
IsHashtag bool
InChannel string
FromUser string
}
var searchFlags = [...]string{"from", "channel", "in"}
func splitWords(text string) []string {
words := []string{}
for _, word := range strings.Fields(text) {
word = puncStart.ReplaceAllString(word, "")
word = puncEnd.ReplaceAllString(word, "")
if len(word) != 0 {
words = append(words, word)
}
}
return words
}
func parseSearchFlags(input []string) ([]string, map[string]string) {
words := []string{}
flags := make(map[string]string)
skipNextWord := false
for i, word := range input {
if skipNextWord {
skipNextWord = false
continue
}
isFlag := false
if colon := strings.Index(word, ":"); colon != -1 {
flag := word[:colon]
value := word[colon+1:]
for _, searchFlag := range searchFlags {
// check for case insensitive equality
if strings.EqualFold(flag, searchFlag) {
if value != "" {
flags[searchFlag] = value
isFlag = true
} else if i < len(input)-1 {
flags[searchFlag] = input[i+1]
skipNextWord = true
isFlag = true
}
if isFlag {
break
}
}
}
}
if !isFlag {
words = append(words, word)
}
}
return words, flags
}
func ParseSearchParams(text string) (*SearchParams, *SearchParams) {
words, flags := parseSearchFlags(splitWords(text))
hashtagTerms := []string{}
plainTerms := []string{}
for _, word := range words {
if validHashtag.MatchString(word) {
hashtagTerms = append(hashtagTerms, word)
} else {
plainTerms = append(plainTerms, word)
}
}
inChannel := flags["channel"]
if inChannel == "" {
inChannel = flags["in"]
}
fromUser := flags["from"]
var plainParams *SearchParams
if len(plainTerms) > 0 {
plainParams = &SearchParams{
Terms: strings.Join(plainTerms, " "),
IsHashtag: false,
InChannel: inChannel,
FromUser: fromUser,
}
}
var hashtagParams *SearchParams
if len(hashtagTerms) > 0 {
hashtagParams = &SearchParams{
Terms: strings.Join(hashtagTerms, " "),
IsHashtag: true,
InChannel: inChannel,
FromUser: fromUser,
}
}
// special case for when no terms are specified but we still have a filter
if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") {
plainParams = &SearchParams{
Terms: "",
IsHashtag: false,
InChannel: inChannel,
FromUser: fromUser,
}
}
return plainParams, hashtagParams
}

70
model/search_params_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"testing"
)
func TestParseSearchFlags(t *testing.T) {
if words, flags := parseSearchFlags(splitWords("")); len(words) != 0 {
t.Fatal("got words from empty input")
} else if len(flags) != 0 {
t.Fatal("got flags from empty input")
}
if words, flags := parseSearchFlags(splitWords("word")); len(words) != 1 || words[0] != "word" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana cherry")); len(words) != 3 || words[0] != "apple" || words[1] != "banana" || words[2] != "cherry" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["from"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["from"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["in"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 1 || flags["channel"] != "chan" {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("fruit: cherry")); len(words) != 2 || words[0] != "fruit:" || words[1] != "cherry" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("channel:")); len(words) != 1 || words[0] != "channel:" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 0 {
t.Fatalf("got incorrect flags %v", flags)
}
if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from:" {
t.Fatalf("got incorrect words %v", words)
} else if len(flags) != 2 || flags["channel"] != "first" || flags["in"] != "second" {
t.Fatalf("got incorrect flags %v", flags)
}
}

Просмотреть файл

@@ -242,10 +242,10 @@ func Etag(parts ...interface{}) string {
var validHashtag = regexp.MustCompile(`^(#[A-Za-z]+[A-Za-z0-9_\-]*[A-Za-z0-9])$`)
var puncStart = regexp.MustCompile(`^[.,()&$!\[\]{}"':;\\]+`)
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"':;\\]+$`)
var puncEnd = regexp.MustCompile(`[.,()&$#!\[\]{}"';\\]+$`)
func ParseHashtags(text string) (string, string) {
words := strings.Split(strings.Replace(text, "\n", " ", -1), " ")
words := strings.Fields(text)
hashtagString := ""
plainString := ""

Просмотреть файл

@@ -407,15 +407,23 @@ var specialSearchChar = []string{
"@",
}
func (s SqlPostStore) Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel {
func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
termMap := map[string]bool{}
terms := params.Terms
if terms == "" && params.InChannel == "" && params.FromUser == "" {
result.Data = []*model.Post{}
storeChannel <- result
return
}
searchType := "Message"
if isHashtagSearch {
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
termMap[term] = true
@@ -430,63 +438,85 @@ func (s SqlPostStore) Search(teamId string, userId string, terms string, isHasht
var posts []*model.Post
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// Parse text for wildcards
if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
}
}
searchQuery := `
SELECT
*
FROM
Posts
WHERE
DeleteAt = 0
POST_FILTER
AND ChannelId IN (
SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId
AND TeamId = :TeamId
AND UserId = :UserId
AND DeleteAt = 0
CHANNEL_FILTER)
SEARCH_CLAUSE
ORDER BY CreateAt DESC
LIMIT 100`
if params.InChannel != "" {
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1)
} else {
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1)
}
if params.FromUser != "" {
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
AND UserId IN (
SELECT
Id
FROM
Users
WHERE
TeamId = :TeamId
AND Username = :FromUser)`, 1)
} else {
searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1)
}
if terms == "" {
// we've already confirmed that we have a channel or user to search for
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// Parse text for wildcards
if wildcard, err := regexp.Compile("\\*($| )"); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
}
searchQuery := fmt.Sprintf(`SELECT
*
FROM
Posts
WHERE
DeleteAt = 0
AND ChannelId IN (SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId AND TeamId = $1
AND UserId = $2
AND DeleteAt = 0)
AND %s @@ to_tsquery($3)
ORDER BY CreateAt DESC
LIMIT 100`, searchType)
terms = strings.Join(strings.Fields(terms), " | ")
_, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms)
if err != nil {
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
}
searchClause := fmt.Sprintf("AND %s @@ to_tsquery(:Terms)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
searchQuery := fmt.Sprintf(`SELECT
*
FROM
Posts
WHERE
DeleteAt = 0
AND ChannelId IN (SELECT
Id
FROM
Channels,
ChannelMembers
WHERE
Id = ChannelId AND TeamId = ?
AND UserId = ?
AND DeleteAt = 0)
AND MATCH (%s) AGAINST (? IN BOOLEAN MODE)
ORDER BY CreateAt DESC
LIMIT 100`, searchType)
searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
}
_, err := s.GetReplica().Select(&posts, searchQuery, teamId, userId, terms)
if err != nil {
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
queryParams := map[string]interface{}{
"TeamId": teamId,
"UserId": userId,
"Terms": terms,
"InChannel": params.InChannel,
"FromUser": params.FromUser,
}
}
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
if err != nil {
result.Err = model.NewAppError("SqlPostStore.Search", "We encounted an error while searching for posts", "teamId="+teamId+", err="+err.Error())
}
list := &model.PostList{Order: make([]string, 0, len(posts))}

Просмотреть файл

@@ -525,57 +525,57 @@ func TestPostStoreSearch(t *testing.T) {
o5.Hashtags = "#secret #howdy"
o5 = (<-store.Post().Save(o5)).Data.(*model.Post)
r1 := (<-store.Post().Search(teamId, userId, "corey", false)).Data.(*model.PostList)
r1 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "corey", IsHashtag: false})).Data.(*model.PostList)
if len(r1.Order) != 1 && r1.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r3 := (<-store.Post().Search(teamId, userId, "new", false)).Data.(*model.PostList)
r3 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "new", IsHashtag: false})).Data.(*model.PostList)
if len(r3.Order) != 2 && r3.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r4 := (<-store.Post().Search(teamId, userId, "john", false)).Data.(*model.PostList)
r4 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "john", IsHashtag: false})).Data.(*model.PostList)
if len(r4.Order) != 1 && r4.Order[0] != o2.Id {
t.Fatal("returned wrong search result")
}
r5 := (<-store.Post().Search(teamId, userId, "matter*", false)).Data.(*model.PostList)
r5 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter*", IsHashtag: false})).Data.(*model.PostList)
if len(r5.Order) != 1 && r5.Order[0] != o1.Id {
t.Fatal("returned wrong search result")
}
r6 := (<-store.Post().Search(teamId, userId, "#hashtag", true)).Data.(*model.PostList)
r6 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#hashtag", IsHashtag: true})).Data.(*model.PostList)
if len(r6.Order) != 1 && r6.Order[0] != o4.Id {
t.Fatal("returned wrong search result")
}
r7 := (<-store.Post().Search(teamId, userId, "#secret", true)).Data.(*model.PostList)
r7 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "#secret", IsHashtag: true})).Data.(*model.PostList)
if len(r7.Order) != 1 && r7.Order[0] != o5.Id {
t.Fatal("returned wrong search result")
}
r8 := (<-store.Post().Search(teamId, userId, "@thisshouldmatchnothing", true)).Data.(*model.PostList)
r8 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true})).Data.(*model.PostList)
if len(r8.Order) != 0 {
t.Fatal("returned wrong search result")
}
r9 := (<-store.Post().Search(teamId, userId, "mattermost jersey", false)).Data.(*model.PostList)
r9 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "mattermost jersey", IsHashtag: false})).Data.(*model.PostList)
if len(r9.Order) != 2 {
t.Fatal("returned wrong search result")
}
r10 := (<-store.Post().Search(teamId, userId, "matter* jer*", false)).Data.(*model.PostList)
r10 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "matter* jer*", IsHashtag: false})).Data.(*model.PostList)
if len(r10.Order) != 2 {
t.Fatal("returned wrong search result")
}
r11 := (<-store.Post().Search(teamId, userId, "message blargh", false)).Data.(*model.PostList)
r11 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "message blargh", IsHashtag: false})).Data.(*model.PostList)
if len(r11.Order) != 1 {
t.Fatal("returned wrong search result")
}
r12 := (<-store.Post().Search(teamId, userId, "blargh>", false)).Data.(*model.PostList)
r12 := (<-store.Post().Search(teamId, userId, &model.SearchParams{Terms: "blargh>", IsHashtag: false})).Data.(*model.PostList)
if len(r12.Order) != 1 {
t.Fatal("returned wrong search result")
}

Просмотреть файл

@@ -84,7 +84,7 @@ type PostStore interface {
GetPosts(channelId string, offset int, limit int) StoreChannel
GetPostsSince(channelId string, time int64) StoreChannel
GetEtag(channelId string) StoreChannel
Search(teamId string, userId string, terms string, isHashtagSearch bool) StoreChannel
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
GetForExport(channelId string) StoreChannel
}