MM-11359: support excluding results from search (#11196)

Этот коммит содержится в:
Siyuan Liu
2019-08-12 05:03:42 -07:00
коммит произвёл Hanzei
родитель a4091cd363
Коммит e4bb8cd887
5 изменённых файлов: 1340 добавлений и 401 удалений

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

@@ -938,6 +938,29 @@ func (a *App) searchPostsInTeam(teamId string, userId string, paramsList []*mode
return posts, nil return posts, nil
} }
func (a *App) convertChannelNamesToChannelIds(channels []string, userId string, teamId string, includeDeletedChannels bool) []string {
for idx, channelName := range channels {
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels)
if err != nil {
mlog.Error(fmt.Sprint(err))
continue
}
channels[idx] = channel.Id
}
return channels
}
func (a *App) convertUserNameToUserIds(usernames []string) []string {
for idx, username := range usernames {
if user, err := a.GetUserByUsername(username); err != nil {
mlog.Error(fmt.Sprint(err))
} else {
usernames[idx] = user.Id
}
}
return usernames
}
func (a *App) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) { func (a *App) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams) (*model.PostList, *model.AppError) {
if !*a.Config().ServiceSettings.EnablePostSearch { if !*a.Config().ServiceSettings.EnablePostSearch {
return nil, model.NewAppError("SearchPostsInTeam", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v", teamId), http.StatusNotImplemented) return nil, model.NewAppError("SearchPostsInTeam", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v", teamId), http.StatusNotImplemented)
@@ -956,23 +979,12 @@ func (a *App) esSearchPostsInTeamForUser(paramsList []*model.SearchParams, userI
// Don't allow users to search for "*" // Don't allow users to search for "*"
if params.Terms != "*" { if params.Terms != "*" {
// Convert channel names to channel IDs // Convert channel names to channel IDs
for idx, channelName := range params.InChannels { params.InChannels = a.convertChannelNamesToChannelIds(params.InChannels, userId, teamId, includeDeletedChannels)
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels) params.ExcludedChannels = a.convertChannelNamesToChannelIds(params.ExcludedChannels, userId, teamId, includeDeletedChannels)
if err != nil {
mlog.Error(fmt.Sprint(err))
continue
}
params.InChannels[idx] = channel.Id
}
// Convert usernames to user IDs // Convert usernames to user IDs
for idx, username := range params.FromUsers { params.FromUsers = a.convertUserNameToUserIds(params.FromUsers)
if user, err := a.GetUserByUsername(username); err != nil { params.ExcludedUsers = a.convertUserNameToUserIds(params.ExcludedUsers)
mlog.Error(fmt.Sprint(err))
} else {
params.FromUsers[idx] = user.Id
}
}
finalParamsList = append(finalParamsList, params) finalParamsList = append(finalParamsList, params)
} }
@@ -1049,6 +1061,16 @@ func (a *App) SearchPostsInTeamForUser(terms string, userId string, teamId strin
params.InChannels[idx] = channel.Name params.InChannels[idx] = channel.Name
} }
} }
for idx, channelName := range params.ExcludedChannels {
if strings.HasPrefix(channelName, "@") {
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels)
if err != nil {
mlog.Error(fmt.Sprint(err))
continue
}
params.ExcludedChannels[idx] = channel.Name
}
}
}) })
if err != nil { if err != nil {
return nil, err return nil, err

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

@@ -14,12 +14,18 @@ var searchTermPuncEnd = regexp.MustCompile(`[^\pL\d\s*"]+$`)
type SearchParams struct { type SearchParams struct {
Terms string Terms string
ExcludedTerms string
IsHashtag bool IsHashtag bool
InChannels []string InChannels []string
ExcludedChannels []string
FromUsers []string FromUsers []string
ExcludedUsers []string
AfterDate string AfterDate string
ExcludedAfterDate string
BeforeDate string BeforeDate string
ExcludedBeforeDate string
OnDate string OnDate string
ExcludedDate string
OrTerms bool OrTerms bool
IncludeDeletedChannels bool IncludeDeletedChannels bool
TimeZoneOffset int TimeZoneOffset int
@@ -40,6 +46,19 @@ func (p *SearchParams) GetAfterDateMillis() int64 {
return GetStartOfDayMillis(afterDate, p.TimeZoneOffset) return GetStartOfDayMillis(afterDate, p.TimeZoneOffset)
} }
// Returns the epoch timestamp of the start of the day specified by SearchParams.ExcludedAfterDate
func (p *SearchParams) GetExcludedAfterDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedAfterDate))
if err != nil {
date = time.Now()
}
// travel forward 1 day
oneDay := time.Hour * 24
afterDate := date.Add(oneDay)
return GetStartOfDayMillis(afterDate, p.TimeZoneOffset)
}
// Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate // Returns the epoch timestamp of the end of the day specified by SearchParams.BeforeDate
func (p *SearchParams) GetBeforeDateMillis() int64 { func (p *SearchParams) GetBeforeDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate)) date, err := time.Parse("2006-01-02", PadDateStringZeros(p.BeforeDate))
@@ -53,6 +72,19 @@ func (p *SearchParams) GetBeforeDateMillis() int64 {
return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset) return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset)
} }
// Returns the epoch timestamp of the end of the day specified by SearchParams.ExcludedBeforeDate
func (p *SearchParams) GetExcludedBeforeDateMillis() int64 {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedBeforeDate))
if err != nil {
return 0
}
// travel back 1 day
oneDay := time.Hour * -24
beforeDate := date.Add(oneDay)
return GetEndOfDayMillis(beforeDate, p.TimeZoneOffset)
}
// Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate // Returns the epoch timestamps of the start and end of the day specified by SearchParams.OnDate
func (p *SearchParams) GetOnDateMillis() (int64, int64) { func (p *SearchParams) GetOnDateMillis() (int64, int64) {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate)) date, err := time.Parse("2006-01-02", PadDateStringZeros(p.OnDate))
@@ -63,8 +95,29 @@ func (p *SearchParams) GetOnDateMillis() (int64, int64) {
return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset) return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset)
} }
// Returns the epoch timestamps of the start and end of the day specified by SearchParams.ExcludedDate
func (p *SearchParams) GetExcludedDateMillis() (int64, int64) {
date, err := time.Parse("2006-01-02", PadDateStringZeros(p.ExcludedDate))
if err != nil {
return 0, 0
}
return GetStartOfDayMillis(date, p.TimeZoneOffset), GetEndOfDayMillis(date, p.TimeZoneOffset)
}
var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on"} var searchFlags = [...]string{"from", "channel", "in", "before", "after", "on"}
type flag struct {
name string
value string
exclude bool
}
type searchWord struct {
value string
exclude bool
}
func splitWords(text string) []string { func splitWords(text string) []string {
words := []string{} words := []string{}
@@ -79,9 +132,13 @@ func splitWords(text string) []string {
foundQuote = false foundQuote = false
location = i + 1 location = i + 1
} else { } else {
words = append(words, strings.Fields(text[location:i])...) nextStart := i
if i > 0 && text[i-1] == '-' {
nextStart = i - 1
}
words = append(words, strings.Fields(text[location:nextStart])...)
foundQuote = true foundQuote = true
location = i location = nextStart
} }
} }
} }
@@ -91,9 +148,9 @@ func splitWords(text string) []string {
return words return words
} }
func parseSearchFlags(input []string) ([]string, [][2]string) { func parseSearchFlags(input []string) ([]searchWord, []flag) {
words := []string{} words := []searchWord{}
flags := [][2]string{} flags := []flag{}
skipNextWord := false skipNextWord := false
for i, word := range input { for i, word := range input {
@@ -105,17 +162,34 @@ func parseSearchFlags(input []string) ([]string, [][2]string) {
isFlag := false isFlag := false
if colon := strings.Index(word, ":"); colon != -1 { if colon := strings.Index(word, ":"); colon != -1 {
flag := word[:colon] var flagName string
var exclude bool
if strings.HasPrefix(word, "-") {
flagName = word[1:colon]
exclude = true
} else {
flagName = word[:colon]
exclude = false
}
value := word[colon+1:] value := word[colon+1:]
for _, searchFlag := range searchFlags { for _, searchFlag := range searchFlags {
// check for case insensitive equality // check for case insensitive equality
if strings.EqualFold(flag, searchFlag) { if strings.EqualFold(flagName, searchFlag) {
if value != "" { if value != "" {
flags = append(flags, [2]string{searchFlag, value}) flags = append(flags, flag{
searchFlag,
value,
exclude,
})
isFlag = true isFlag = true
} else if i < len(input)-1 { } else if i < len(input)-1 {
flags = append(flags, [2]string{searchFlag, input[i+1]}) flags = append(flags, flag{
searchFlag,
input[i+1],
exclude,
})
skipNextWord = true skipNextWord = true
isFlag = true isFlag = true
} }
@@ -128,6 +202,10 @@ func parseSearchFlags(input []string) ([]string, [][2]string) {
} }
if !isFlag { if !isFlag {
exclude := false
if strings.HasPrefix(word, "-") {
exclude = true
}
// trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards) // trim off surrounding punctuation (note that we leave trailing asterisks to allow wildcards)
word = searchTermPuncStart.ReplaceAllString(word, "") word = searchTermPuncStart.ReplaceAllString(word, "")
word = searchTermPuncEnd.ReplaceAllString(word, "") word = searchTermPuncEnd.ReplaceAllString(word, "")
@@ -136,7 +214,10 @@ func parseSearchFlags(input []string) ([]string, [][2]string) {
word = hashtagStart.ReplaceAllString(word, "#") word = hashtagStart.ReplaceAllString(word, "#")
if len(word) != 0 { if len(word) != 0 {
words = append(words, word) words = append(words, searchWord{
word,
exclude,
})
} }
} }
} }
@@ -148,81 +229,139 @@ func ParseSearchParams(text string, timeZoneOffset int) []*SearchParams {
words, flags := parseSearchFlags(splitWords(text)) words, flags := parseSearchFlags(splitWords(text))
hashtagTermList := []string{} hashtagTermList := []string{}
excludedHashtagTermList := []string{}
plainTermList := []string{} plainTermList := []string{}
excludedPlainTermList := []string{}
for _, word := range words { for _, word := range words {
if validHashtag.MatchString(word) { if validHashtag.MatchString(word.value) {
hashtagTermList = append(hashtagTermList, word) if word.exclude {
excludedHashtagTermList = append(excludedHashtagTermList, word.value)
} else {
hashtagTermList = append(hashtagTermList, word.value)
}
} else { } else {
plainTermList = append(plainTermList, word) if word.exclude {
excludedPlainTermList = append(excludedPlainTermList, word.value)
} else {
plainTermList = append(plainTermList, word.value)
}
} }
} }
hashtagTerms := strings.Join(hashtagTermList, " ") hashtagTerms := strings.Join(hashtagTermList, " ")
excludedHashtagTerms := strings.Join(excludedHashtagTermList, " ")
plainTerms := strings.Join(plainTermList, " ") plainTerms := strings.Join(plainTermList, " ")
excludedPlainTerms := strings.Join(excludedPlainTermList, " ")
inChannels := []string{} inChannels := []string{}
excludedChannels := []string{}
fromUsers := []string{} fromUsers := []string{}
excludedUsers := []string{}
afterDate := "" afterDate := ""
excludedAfterDate := ""
beforeDate := "" beforeDate := ""
excludedBeforeDate := ""
onDate := "" onDate := ""
excludedDate := ""
for _, flagPair := range flags { for _, flag := range flags {
flag := flagPair[0] if flag.name == "in" || flag.name == "channel" {
value := flagPair[1] if flag.exclude {
excludedChannels = append(excludedChannels, flag.value)
if flag == "in" || flag == "channel" { } else {
inChannels = append(inChannels, value) inChannels = append(inChannels, flag.value)
} else if flag == "from" { }
fromUsers = append(fromUsers, value) } else if flag.name == "from" {
} else if flag == "after" { if flag.exclude {
afterDate = value excludedUsers = append(excludedUsers, flag.value)
} else if flag == "before" { } else {
beforeDate = value fromUsers = append(fromUsers, flag.value)
} else if flag == "on" { }
onDate = value } else if flag.name == "after" {
if flag.exclude {
excludedAfterDate = flag.value
} else {
afterDate = flag.value
}
} else if flag.name == "before" {
if flag.exclude {
excludedBeforeDate = flag.value
} else {
beforeDate = flag.value
}
} else if flag.name == "on" {
if flag.exclude {
excludedDate = flag.value
} else {
onDate = flag.value
}
} }
} }
paramsList := []*SearchParams{} paramsList := []*SearchParams{}
if len(plainTerms) > 0 { if len(plainTerms) > 0 || len(excludedPlainTerms) > 0 {
paramsList = append(paramsList, &SearchParams{ paramsList = append(paramsList, &SearchParams{
Terms: plainTerms, Terms: plainTerms,
IsHashtag: false, ExcludedTerms: excludedPlainTerms,
InChannels: inChannels, IsHashtag: false,
FromUsers: fromUsers, InChannels: inChannels,
AfterDate: afterDate, ExcludedChannels: excludedChannels,
BeforeDate: beforeDate, FromUsers: fromUsers,
OnDate: onDate, ExcludedUsers: excludedUsers,
TimeZoneOffset: timeZoneOffset, AfterDate: afterDate,
ExcludedAfterDate: excludedAfterDate,
BeforeDate: beforeDate,
ExcludedBeforeDate: excludedBeforeDate,
OnDate: onDate,
ExcludedDate: excludedDate,
TimeZoneOffset: timeZoneOffset,
}) })
} }
if len(hashtagTerms) > 0 { if len(hashtagTerms) > 0 || len(excludedHashtagTerms) > 0 {
paramsList = append(paramsList, &SearchParams{ paramsList = append(paramsList, &SearchParams{
Terms: hashtagTerms, Terms: hashtagTerms,
IsHashtag: true, ExcludedTerms: excludedHashtagTerms,
InChannels: inChannels, IsHashtag: true,
FromUsers: fromUsers, InChannels: inChannels,
AfterDate: afterDate, ExcludedChannels: excludedChannels,
BeforeDate: beforeDate, FromUsers: fromUsers,
OnDate: onDate, ExcludedUsers: excludedUsers,
TimeZoneOffset: timeZoneOffset, AfterDate: afterDate,
ExcludedAfterDate: excludedAfterDate,
BeforeDate: beforeDate,
ExcludedBeforeDate: excludedBeforeDate,
OnDate: onDate,
ExcludedDate: excludedDate,
TimeZoneOffset: timeZoneOffset,
}) })
} }
// special case for when no terms are specified but we still have a filter // special case for when no terms are specified but we still have a filter
if len(plainTerms) == 0 && len(hashtagTerms) == 0 && (len(inChannels) != 0 || len(fromUsers) != 0 || len(afterDate) != 0 || len(beforeDate) != 0 || len(onDate) != 0) { if len(plainTerms) == 0 && len(hashtagTerms) == 0 &&
len(excludedPlainTerms) == 0 && len(excludedHashtagTerms) == 0 &&
(len(inChannels) != 0 || len(fromUsers) != 0 ||
len(excludedChannels) != 0 || len(excludedUsers) != 0 ||
len(afterDate) != 0 || len(excludedAfterDate) != 0 ||
len(beforeDate) != 0 || len(excludedBeforeDate) != 0 ||
len(onDate) != 0 || len(excludedDate) != 0) {
paramsList = append(paramsList, &SearchParams{ paramsList = append(paramsList, &SearchParams{
Terms: "", Terms: "",
IsHashtag: false, ExcludedTerms: "",
InChannels: inChannels, IsHashtag: false,
FromUsers: fromUsers, InChannels: inChannels,
AfterDate: afterDate, ExcludedChannels: excludedChannels,
BeforeDate: beforeDate, FromUsers: fromUsers,
OnDate: onDate, ExcludedUsers: excludedUsers,
TimeZoneOffset: timeZoneOffset, AfterDate: afterDate,
ExcludedAfterDate: excludedAfterDate,
BeforeDate: beforeDate,
ExcludedBeforeDate: excludedBeforeDate,
OnDate: onDate,
ExcludedDate: excludedDate,
TimeZoneOffset: timeZoneOffset,
}) })
} }

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -799,33 +799,136 @@ var specialSearchChar = []string{
":", ":",
} }
func (s *SqlPostStore) buildCreateDateFilterClause(params *model.SearchParams, queryParams map[string]interface{}) (string, map[string]interface{}) {
searchQuery := ""
// handle after: before: on: filters
if len(params.OnDate) > 0 {
onDateStart, onDateEnd := params.GetOnDateMillis()
queryParams["OnDateStart"] = strconv.FormatInt(onDateStart, 10)
queryParams["OnDateEnd"] = strconv.FormatInt(onDateEnd, 10)
// between `on date` start of day and end of day
searchQuery += "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd "
} else {
if len(params.ExcludedDate) > 0 {
excludedDateStart, excludedDateEnd := params.GetExcludedDateMillis()
queryParams["ExcludedDateStart"] = strconv.FormatInt(excludedDateStart, 10)
queryParams["ExcludedDateEnd"] = strconv.FormatInt(excludedDateEnd, 10)
searchQuery += "AND CreateAt NOT BETWEEN :ExcludedDateStart AND :ExcludedDateEnd "
}
if len(params.AfterDate) > 0 {
afterDate := params.GetAfterDateMillis()
queryParams["AfterDate"] = strconv.FormatInt(afterDate, 10)
// greater than `after date`
searchQuery += "AND CreateAt >= :AfterDate "
}
if len(params.BeforeDate) > 0 {
beforeDate := params.GetBeforeDateMillis()
queryParams["BeforeDate"] = strconv.FormatInt(beforeDate, 10)
// less than `before date`
searchQuery += "AND CreateAt <= :BeforeDate "
}
if len(params.ExcludedAfterDate) > 0 {
afterDate := params.GetExcludedAfterDateMillis()
queryParams["ExcludedAfterDate"] = strconv.FormatInt(afterDate, 10)
searchQuery += "AND CreateAt < :ExcludedAfterDate "
}
if len(params.ExcludedBeforeDate) > 0 {
beforeDate := params.GetExcludedBeforeDateMillis()
queryParams["ExcludedBeforeDate"] = strconv.FormatInt(beforeDate, 10)
searchQuery += "AND CreateAt > :ExcludedBeforeDate "
}
}
return searchQuery, queryParams
}
func (s *SqlPostStore) buildSearchChannelFilterClause(channels []string, paramPrefix string, exclusion bool, queryParams map[string]interface{}) (string, map[string]interface{}) {
if len(channels) == 0 {
return "", queryParams
}
clauseSlice := []string{}
for i, channel := range channels {
paramName := paramPrefix + strconv.FormatInt(int64(i), 10)
clauseSlice = append(clauseSlice, ":"+paramName)
queryParams[paramName] = channel
}
clause := strings.Join(clauseSlice, ", ")
if exclusion {
return "AND Name NOT IN (" + clause + ")", queryParams
}
return "AND Name IN (" + clause + ")", queryParams
}
func (s *SqlPostStore) buildSearchUserFilterClause(users []string, paramPrefix string, exclusion bool, queryParams map[string]interface{}) (string, map[string]interface{}) {
if len(users) == 0 {
return "", queryParams
}
clauseSlice := []string{}
for i, user := range users {
paramName := paramPrefix + strconv.FormatInt(int64(i), 10)
clauseSlice = append(clauseSlice, ":"+paramName)
queryParams[paramName] = user
}
clause := strings.Join(clauseSlice, ", ")
if exclusion {
return "AND Username NOT IN (" + clause + ")", queryParams
}
return "AND Username IN (" + clause + ")", queryParams
}
func (s *SqlPostStore) buildSearchPostFilterClause(fromUsers []string, excludedUsers []string, queryParams map[string]interface{}) (string, map[string]interface{}) {
if len(fromUsers) == 0 && len(excludedUsers) == 0 {
return "", queryParams
}
filterQuery := `
AND UserId IN (
SELECT
Id
FROM
Users,
TeamMembers
WHERE
TeamMembers.TeamId = :TeamId
AND Users.Id = TeamMembers.UserId
FROM_USER_FILTER
EXCLUDED_USER_FILTER)`
fromUserClause, queryParams := s.buildSearchUserFilterClause(fromUsers, "FromUser", false, queryParams)
filterQuery = strings.Replace(filterQuery, "FROM_USER_FILTER", fromUserClause, 1)
excludedUserClause, queryParams := s.buildSearchUserFilterClause(excludedUsers, "ExcludedUser", true, queryParams)
filterQuery = strings.Replace(filterQuery, "EXCLUDED_USER_FILTER", excludedUserClause, 1)
return filterQuery, queryParams
}
func (s *SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) (*model.PostList, *model.AppError) { func (s *SqlPostStore) Search(teamId string, userId string, params *model.SearchParams) (*model.PostList, *model.AppError) {
queryParams := map[string]interface{}{ queryParams := map[string]interface{}{
"TeamId": teamId, "TeamId": teamId,
"UserId": userId, "UserId": userId,
} }
termMap := map[string]bool{}
terms := params.Terms
list := model.NewPostList() list := model.NewPostList()
if params.Terms == "" && params.ExcludedTerms == "" &&
if terms == "" && len(params.InChannels) == 0 && len(params.FromUsers) == 0 && len(params.OnDate) == 0 && len(params.AfterDate) == 0 && len(params.BeforeDate) == 0 { len(params.InChannels) == 0 && len(params.ExcludedChannels) == 0 &&
len(params.FromUsers) == 0 && len(params.ExcludedUsers) == 0 &&
len(params.OnDate) == 0 && len(params.AfterDate) == 0 && len(params.BeforeDate) == 0 {
return list, nil return list, nil
} }
searchType := "Message"
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
termMap[strings.ToUpper(term)] = true
}
}
// these chars have special meaning and can be treated as spaces
for _, c := range specialSearchChar {
terms = strings.Replace(terms, c, " ", -1)
}
var posts []*model.Post var posts []*model.Post
deletedQueryPart := "AND DeleteAt = 0" deletedQueryPart := "AND DeleteAt = 0"
@@ -858,116 +961,62 @@ func (s *SqlPostStore) Search(teamId string, userId string, params *model.Search
AND (TeamId = :TeamId OR TeamId = '') AND (TeamId = :TeamId OR TeamId = '')
` + userIdPart + ` ` + userIdPart + `
` + deletedQueryPart + ` ` + deletedQueryPart + `
CHANNEL_FILTER) IN_CHANNEL_FILTER
EXCLUDED_CHANNEL_FILTER)
CREATEDATE_CLAUSE CREATEDATE_CLAUSE
SEARCH_CLAUSE SEARCH_CLAUSE
ORDER BY CreateAt DESC ORDER BY CreateAt DESC
LIMIT 100` LIMIT 100`
if len(params.InChannels) > 1 { inChannelClause, queryParams := s.buildSearchChannelFilterClause(params.InChannels, "InChannel", false, queryParams)
inClause := ":InChannel0" searchQuery = strings.Replace(searchQuery, "IN_CHANNEL_FILTER", inChannelClause, 1)
queryParams["InChannel0"] = params.InChannels[0]
for i := 1; i < len(params.InChannels); i++ { excludedChannelClause, queryParams := s.buildSearchChannelFilterClause(params.ExcludedChannels, "ExcludedChannel", true, queryParams)
paramName := "InChannel" + strconv.FormatInt(int64(i), 10) searchQuery = strings.Replace(searchQuery, "EXCLUDED_CHANNEL_FILTER", excludedChannelClause, 1)
inClause += ", :" + paramName
queryParams[paramName] = params.InChannels[i] postFilterClause, queryParams := s.buildSearchPostFilterClause(params.FromUsers, params.ExcludedUsers, queryParams)
searchQuery = strings.Replace(searchQuery, "POST_FILTER", postFilterClause, 1)
createDateFilterClause, queryParams := s.buildCreateDateFilterClause(params, queryParams)
searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", createDateFilterClause, 1)
termMap := map[string]bool{}
terms := params.Terms
excludedTerms := params.ExcludedTerms
searchType := "Message"
if params.IsHashtag {
searchType = "Hashtags"
for _, term := range strings.Split(terms, " ") {
termMap[strings.ToUpper(term)] = true
} }
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name IN ("+inClause+")", 1)
} else if len(params.InChannels) == 1 {
queryParams["InChannel"] = params.InChannels[0]
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1)
} else {
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1)
} }
if len(params.FromUsers) > 1 { // these chars have special meaning and can be treated as spaces
inClause := ":FromUser0" for _, c := range specialSearchChar {
queryParams["FromUser0"] = params.FromUsers[0] terms = strings.Replace(terms, c, " ", -1)
excludedTerms = strings.Replace(excludedTerms, c, " ", -1)
for i := 1; i < len(params.FromUsers); i++ {
paramName := "FromUser" + strconv.FormatInt(int64(i), 10)
inClause += ", :" + paramName
queryParams[paramName] = params.FromUsers[i]
}
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
AND UserId IN (
SELECT
Id
FROM
Users,
TeamMembers
WHERE
TeamMembers.TeamId = :TeamId
AND Users.Id = TeamMembers.UserId
AND Username IN (`+inClause+`))`, 1)
} else if len(params.FromUsers) == 1 {
queryParams["FromUser"] = params.FromUsers[0]
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
AND UserId IN (
SELECT
Id
FROM
Users,
TeamMembers
WHERE
TeamMembers.TeamId = :TeamId
AND Users.Id = TeamMembers.UserId
AND Username = :FromUser)`, 1)
} else {
searchQuery = strings.Replace(searchQuery, "POST_FILTER", "", 1)
} }
// handle after: before: on: filters if terms == "" && excludedTerms == "" {
if len(params.AfterDate) > 1 || len(params.BeforeDate) > 1 || len(params.OnDate) > 1 {
if len(params.OnDate) > 1 {
onDateStart, onDateEnd := params.GetOnDateMillis()
queryParams["OnDateStart"] = strconv.FormatInt(onDateStart, 10)
queryParams["OnDateEnd"] = strconv.FormatInt(onDateEnd, 10)
// between `on date` start of day and end of day
searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd ", 1)
} else if len(params.AfterDate) > 1 && len(params.BeforeDate) > 1 {
afterDate := params.GetAfterDateMillis()
beforeDate := params.GetBeforeDateMillis()
queryParams["OnDateStart"] = strconv.FormatInt(afterDate, 10)
queryParams["OnDateEnd"] = strconv.FormatInt(beforeDate, 10)
// between clause
searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt BETWEEN :OnDateStart AND :OnDateEnd ", 1)
} else if len(params.AfterDate) > 1 {
afterDate := params.GetAfterDateMillis()
queryParams["AfterDate"] = strconv.FormatInt(afterDate, 10)
// greater than `after date`
searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt >= :AfterDate ", 1)
} else if len(params.BeforeDate) > 1 {
beforeDate := params.GetBeforeDateMillis()
queryParams["BeforeDate"] = strconv.FormatInt(beforeDate, 10)
// less than `before date`
searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "AND CreateAt <= :BeforeDate ", 1)
}
} else {
// no create date filters set
searchQuery = strings.Replace(searchQuery, "CREATEDATE_CLAUSE", "", 1)
}
if terms == "" {
// we've already confirmed that we have a channel or user to search for // we've already confirmed that we have a channel or user to search for
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1)
} else if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { } else if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
// Parse text for wildcards // Parse text for wildcards
if wildcard, err := regexp.Compile(`\*($| )`); err == nil { if wildcard, err := regexp.Compile(`\*($| )`); err == nil {
terms = wildcard.ReplaceAllLiteralString(terms, ":* ") terms = wildcard.ReplaceAllLiteralString(terms, ":* ")
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
}
excludeClause := ""
if excludedTerms != "" {
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
} }
if params.OrTerms { if params.OrTerms {
terms = strings.Join(strings.Fields(terms), " | ") queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
} else { } else {
terms = strings.Join(strings.Fields(terms), " & ") queryParams["Terms"] = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
} }
searchClause := fmt.Sprintf("AND to_tsvector('english', %s) @@ to_tsquery(:Terms)", searchType) searchClause := fmt.Sprintf("AND to_tsvector('english', %s) @@ to_tsquery(:Terms)", searchType)
@@ -976,43 +1025,45 @@ func (s *SqlPostStore) Search(teamId string, userId string, params *model.Search
searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType) searchClause := fmt.Sprintf("AND MATCH (%s) AGAINST (:Terms IN BOOLEAN MODE)", searchType)
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1) searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
if !params.OrTerms { excludeClause := ""
splitTerms := strings.Fields(terms) if excludedTerms != "" {
for i, t := range strings.Fields(terms) { excludeClause = " -(" + excludedTerms + ")"
splitTerms[i] = "+" + t }
}
terms = strings.Join(splitTerms, " ") if params.OrTerms {
queryParams["Terms"] = terms + excludeClause
} else {
splitTerms := []string{}
for _, t := range strings.Fields(terms) {
splitTerms = append(splitTerms, "+"+t)
}
queryParams["Terms"] = strings.Join(splitTerms, " ") + excludeClause
} }
} }
queryParams["Terms"] = terms
_, err := s.GetSearchReplica().Select(&posts, searchQuery, queryParams) _, err := s.GetSearchReplica().Select(&posts, searchQuery, queryParams)
if err != nil { if err != nil {
mlog.Warn(fmt.Sprintf("Query error searching posts: %v", err.Error())) mlog.Warn(fmt.Sprintf("Query error searching posts: %v", err.Error()))
// Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results. // Don't return the error to the caller as it is of no use to the user. Instead return an empty set of search results.
return list, nil } else {
} for _, p := range posts {
if searchType == "Hashtags" {
for _, p := range posts { exactMatch := false
if searchType == "Hashtags" { for _, tag := range strings.Split(p.Hashtags, " ") {
exactMatch := false if termMap[strings.ToUpper(tag)] {
for _, tag := range strings.Split(p.Hashtags, " ") { exactMatch = true
if termMap[strings.ToUpper(tag)] { break
exactMatch = true }
}
if !exactMatch {
continue
} }
} }
if !exactMatch { list.AddPost(p)
continue list.AddOrder(p.Id)
}
} }
list.AddPost(p)
list.AddOrder(p.Id)
} }
list.MakeNonNil() list.MakeNonNil()
return list, nil return list, nil
} }

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

@@ -1170,10 +1170,46 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
teamId := model.NewId() teamId := model.NewId()
userId := model.NewId() userId := model.NewId()
u1 := &model.User{}
u1.Username = "usera1"
u1.Email = MakeEmail()
u1, err := ss.User().Save(u1)
require.Nil(t, err)
t1 := &model.TeamMember{}
t1.TeamId = teamId
t1.UserId = u1.Id
_, err = ss.Team().SaveMember(t1, 1000)
require.Nil(t, err)
u2 := &model.User{}
u2.Username = "userb2"
u2.Email = MakeEmail()
u2, err = ss.User().Save(u2)
require.Nil(t, err)
t2 := &model.TeamMember{}
t2.TeamId = teamId
t2.UserId = u2.Id
_, err = ss.Team().SaveMember(t2, 1000)
require.Nil(t, err)
u3 := &model.User{}
u3.Username = "userc3"
u3.Email = MakeEmail()
u3, err = ss.User().Save(u3)
require.Nil(t, err)
t3 := &model.TeamMember{}
t3.TeamId = teamId
t3.UserId = u3.Id
_, err = ss.Team().SaveMember(t3, 1000)
require.Nil(t, err)
c1 := &model.Channel{} c1 := &model.Channel{}
c1.TeamId = teamId c1.TeamId = teamId
c1.DisplayName = "Channel1" c1.DisplayName = "Channel1"
c1.Name = "zz" + model.NewId() + "b" c1.Name = "channel-x"
c1.Type = model.CHANNEL_OPEN c1.Type = model.CHANNEL_OPEN
c1, _ = ss.Channel().Save(c1, -1) c1, _ = ss.Channel().Save(c1, -1)
@@ -1181,20 +1217,20 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
m1.ChannelId = c1.Id m1.ChannelId = c1.Id
m1.UserId = userId m1.UserId = userId
m1.NotifyProps = model.GetDefaultChannelNotifyProps() m1.NotifyProps = model.GetDefaultChannelNotifyProps()
_, err := ss.Channel().SaveMember(&m1) _, err = ss.Channel().SaveMember(&m1)
require.Nil(t, err) require.Nil(t, err)
c2 := &model.Channel{} c2 := &model.Channel{}
c2.TeamId = teamId c2.TeamId = teamId
c2.DisplayName = "Channel1" c2.DisplayName = "Channel2"
c2.Name = "zz" + model.NewId() + "b" c2.Name = "channel-y"
c2.Type = model.CHANNEL_OPEN c2.Type = model.CHANNEL_OPEN
c2, _ = ss.Channel().Save(c2, -1) c2, _ = ss.Channel().Save(c2, -1)
c3 := &model.Channel{} c3 := &model.Channel{}
c3.TeamId = teamId c3.TeamId = teamId
c3.DisplayName = "Channel1" c3.DisplayName = "Channel3"
c3.Name = "zz" + model.NewId() + "b" c3.Name = "channel-z"
c3.Type = model.CHANNEL_OPEN c3.Type = model.CHANNEL_OPEN
c3, _ = ss.Channel().Save(c3, -1) c3, _ = ss.Channel().Save(c3, -1)
@@ -1209,37 +1245,37 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
o1 := &model.Post{} o1 := &model.Post{}
o1.ChannelId = c1.Id o1.ChannelId = c1.Id
o1.UserId = model.NewId() o1.UserId = u1.Id
o1.Message = "corey mattermost new york" o1.Message = "corey mattermost new york United States"
o1, err = ss.Post().Save(o1) o1, err = ss.Post().Save(o1)
require.Nil(t, err) require.Nil(t, err)
o1a := &model.Post{} o1a := &model.Post{}
o1a.ChannelId = c1.Id o1a.ChannelId = c1.Id
o1a.UserId = model.NewId() o1a.UserId = model.NewId()
o1a.Message = "corey mattermost new york" o1a.Message = "corey mattermost new york United States"
o1a.Type = model.POST_JOIN_CHANNEL o1a.Type = model.POST_JOIN_CHANNEL
_, err = ss.Post().Save(o1a) _, err = ss.Post().Save(o1a)
require.Nil(t, err) require.Nil(t, err)
o2 := &model.Post{} o2 := &model.Post{}
o2.ChannelId = c1.Id o2.ChannelId = c1.Id
o2.UserId = model.NewId() o2.UserId = u2.Id
o2.Message = "New Jersey is where John is from" o2.Message = "New Jersey United States is where John is from"
o2, err = ss.Post().Save(o2) o2, err = ss.Post().Save(o2)
require.Nil(t, err) require.Nil(t, err)
o3 := &model.Post{} o3 := &model.Post{}
o3.ChannelId = c2.Id o3.ChannelId = c2.Id
o3.UserId = model.NewId() o3.UserId = model.NewId()
o3.Message = "New Jersey is where John is from corey new york" o3.Message = "New Jersey United States is where John is from corey new york"
_, err = ss.Post().Save(o3) _, err = ss.Post().Save(o3)
require.Nil(t, err) require.Nil(t, err)
o4 := &model.Post{} o4 := &model.Post{}
o4.ChannelId = c1.Id o4.ChannelId = c1.Id
o4.UserId = model.NewId() o4.UserId = model.NewId()
o4.Hashtags = "#hashtag" o4.Hashtags = "#hashtag #tagme"
o4.Message = "(message)blargh" o4.Message = "(message)blargh"
o4, err = ss.Post().Save(o4) o4, err = ss.Post().Save(o4)
require.Nil(t, err) require.Nil(t, err)
@@ -1247,7 +1283,7 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
o5 := &model.Post{} o5 := &model.Post{}
o5.ChannelId = c1.Id o5.ChannelId = c1.Id
o5.UserId = model.NewId() o5.UserId = model.NewId()
o5.Hashtags = "#secret #howdy" o5.Hashtags = "#secret #howdy #tagme"
o5, err = ss.Post().Save(o5) o5, err = ss.Post().Save(o5)
require.Nil(t, err) require.Nil(t, err)
@@ -1260,8 +1296,8 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
o7 := &model.Post{} o7 := &model.Post{}
o7.ChannelId = c3.Id o7.ChannelId = c3.Id
o7.UserId = model.NewId() o7.UserId = u3.Id
o7.Message = "New Jersey is where John is from corey new york" o7.Message = "New Jersey United States is where John is from corey new york"
o7, err = ss.Post().Save(o7) o7, err = ss.Post().Save(o7)
require.Nil(t, err) require.Nil(t, err)
@@ -1314,6 +1350,12 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
1, 1,
[]string{o5.Id}, []string{o5.Id},
}, },
{
"hashtag-search-with-exclusion",
&model.SearchParams{Terms: "#tagme", ExcludedTerms: "#hashtag", IsHashtag: true},
1,
[]string{o5.Id},
},
{ {
"no-match-mention", "no-match-mention",
&model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true}, &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true},
@@ -1326,18 +1368,48 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
0, 0,
[]string{}, []string{},
}, },
{
"exclude-search",
&model.SearchParams{Terms: "united", ExcludedTerms: "jersey"},
1,
[]string{o1.Id},
},
{ {
"multiple-words-search", "multiple-words-search",
&model.SearchParams{Terms: "corey new york"}, &model.SearchParams{Terms: "corey new york"},
1, 1,
[]string{o1.Id}, []string{o1.Id},
}, },
{
"multiple-words-with-exclusion-search",
&model.SearchParams{Terms: "united states", ExcludedTerms: "jersey"},
1,
[]string{o1.Id},
},
{
"multiple-excluded-words-search",
&model.SearchParams{Terms: "united", ExcludedTerms: "corey john"},
0,
[]string{},
},
{ {
"multiple-wildcard-search", "multiple-wildcard-search",
&model.SearchParams{Terms: "matter* jer*"}, &model.SearchParams{Terms: "matter* jer*"},
0, 0,
[]string{}, []string{},
}, },
{
"multiple-wildcard-with-exclusion-search",
&model.SearchParams{Terms: "unite* state*", ExcludedTerms: "jers*"},
1,
[]string{o1.Id},
},
{
"multiple-wildcard-excluded-words-search",
&model.SearchParams{Terms: "united states", ExcludedTerms: "jers* yor*"},
0,
[]string{},
},
{ {
"search-with-work-next-to-a-symbol", "search-with-work-next-to-a-symbol",
&model.SearchParams{Terms: "message blargh"}, &model.SearchParams{Terms: "message blargh"},
@@ -1350,6 +1422,60 @@ func testPostStoreSearch(t *testing.T, ss store.Store) {
2, 2,
[]string{o1.Id, o2.Id}, []string{o1.Id, o2.Id},
}, },
{
"exclude-search-with-or",
&model.SearchParams{Terms: "york jersey", ExcludedTerms: "john", OrTerms: true},
1,
[]string{o1.Id},
},
{
"search-with-from-user",
&model.SearchParams{Terms: "united states", FromUsers: []string{"usera1"}, IncludeDeletedChannels: true},
1,
[]string{o1.Id},
},
{
"search-with-multiple-from-user",
&model.SearchParams{Terms: "united states", FromUsers: []string{"usera1", "userc3"}, IncludeDeletedChannels: true},
2,
[]string{o1.Id, o7.Id},
},
{
"search-with-excluded-user",
&model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1"}, IncludeDeletedChannels: true},
2,
[]string{o2.Id, o7.Id},
},
{
"search-with-multiple-excluded-user",
&model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1", "userb2"}, IncludeDeletedChannels: true},
1,
[]string{o7.Id},
},
{
"search-with-deleted-and-channel-filter",
&model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true},
2,
[]string{o1.Id, o2.Id},
},
{
"search-with-deleted-and-multiple-channel-filter",
&model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true},
3,
[]string{o1.Id, o2.Id, o7.Id},
},
{
"search-with-deleted-and-excluded-channel-filter",
&model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true},
1,
[]string{o7.Id},
},
{
"search-with-deleted-and-multiple-excluded-channel-filter",
&model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true},
0,
[]string{},
},
{ {
"search-with-or-and-deleted", "search-with-or-and-deleted",
&model.SearchParams{Terms: "Jersey corey", OrTerms: true, IncludeDeletedChannels: true}, &model.SearchParams{Terms: "Jersey corey", OrTerms: true, IncludeDeletedChannels: true},