Skip to content

Commit

Permalink
[MM-54357] Recent Mentions is showing posts for other similar named u…
Browse files Browse the repository at this point in the history
…sers. (#25010) (#25285)

* Handle double quotes in Postgres
* quote the username when performing the search

(cherry picked from commit dfb561a)

Co-authored-by: Mattermost Build <build@mattermost.com>
  • Loading branch information
vish9812 and mattermost-build committed Nov 6, 2023
1 parent b266aed commit b245d96
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 24 deletions.
109 changes: 96 additions & 13 deletions store/searchtest/post_layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ var searchPostStoreTests = []searchTest{
Tags: []string{EngineAll},
},
{
Name: "Should be able to search for exact phrases in quotes",
Fn: testSearchExactPhraseInQuotes,
Name: "Should be able to search for quoted patterns with AND OR combinations",
Fn: testSearchANDORQuotesCombinations,
Tags: []string{EnginePostgres, EngineMySql, EngineElasticSearch},
},
{
Expand Down Expand Up @@ -347,24 +347,107 @@ func testSearchReturnPinnedAndUnpinned(t *testing.T, th *SearchTestHelper) {
th.checkPostInSearchResults(t, p2.Id, results.Posts)
}

func testSearchExactPhraseInQuotes(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test 1 2 3", "", model.PostTypeDefault, 0, false)
func testSearchANDORQuotesCombinations(t *testing.T, th *SearchTestHelper) {
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one two three four", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test 123", "", model.PostTypeDefault, 0, false)
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one two five", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel something test 1 2 3", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel 1 2 3", "", model.PostTypeDefault, 0, false)
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one five six", "", model.PostTypeDefault, 0, false)
require.NoError(t, err)

defer th.deleteUserPosts(th.User.Id)

params := &model.SearchParams{Terms: "\"channel test 1 2 3\""}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)
testCases := []struct {
name string
terms string
orTerms bool
expectedLen int
expectedIDs []string
}{
{
name: "AND operator, No Quotes, Matches 1",
terms: `two four`,
orTerms: false,
expectedLen: 1,
expectedIDs: []string{p1.Id},
},
{
name: "AND operator, No Quotes, Matches 0",
terms: `two six`,
orTerms: false,
expectedLen: 0,
expectedIDs: []string{},
},
{
name: "AND operator, With Full Quotes, Matches 0",
terms: `"two four"`,
orTerms: false,
expectedLen: 0,
expectedIDs: []string{},
},
{
name: "AND operator, With Full Quotes, Matches 1",
terms: `"two three four"`,
orTerms: false,
expectedLen: 1,
expectedIDs: []string{p1.Id},
},
{
name: "AND operator, With Part Quotes, Matches 1",
terms: `two "three four"`,
orTerms: false,
expectedLen: 1,
expectedIDs: []string{p1.Id},
},
{
name: "OR operator, No Quotes, Matches 2",
terms: `two four`,
orTerms: true,
expectedLen: 2,
expectedIDs: []string{p1.Id, p2.Id},
},
{
name: "OR operator, No Quotes, Matches 3",
terms: `two six`,
orTerms: true,
expectedLen: 3,
expectedIDs: []string{p1.Id, p2.Id, p3.Id},
},
{
name: "OR operator, With Full Quotes, Matches 0",
terms: `"two four"`,
orTerms: true,
expectedLen: 0,
expectedIDs: []string{},
},
{
name: "OR operator, With Full Quotes, Matches 1",
terms: `"two three four"`,
orTerms: true,
expectedLen: 1,
expectedIDs: []string{p1.Id},
},
{
name: "OR operator, With Part Quotes, Matches 2",
terms: `two "three four"`,
orTerms: true,
expectedLen: 2,
expectedIDs: []string{p1.Id, p2.Id},
},
}

require.Len(t, results.Posts, 1)
th.checkPostInSearchResults(t, p1.Id, results.Posts)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
params := &model.SearchParams{Terms: tc.terms, OrTerms: tc.orTerms}
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
require.NoError(t, err)

require.Len(t, results.Posts, tc.expectedLen)
for _, id := range tc.expectedIDs {
th.checkPostInSearchResults(t, id, results.Posts)
}
})
}
}

func testSearchEmailAddresses(t *testing.T, th *SearchTestHelper) {
Expand Down
41 changes: 30 additions & 11 deletions store/sqlstore/post_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import (
"github.com/mattermost/mattermost-server/v6/utils"
)

// Regex to get quoted strings
var quotedStringsRegex = regexp.MustCompile(`("[^"]*")`)

type SqlPostStore struct {
*SqlStore
metrics einterfaces.MetricsInterface
Expand Down Expand Up @@ -2029,22 +2032,38 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
}

excludeClause := ""
if excludedTerms != "" {
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
// Replace spaces with to_tsquery symbols
replaceSpaces := func(input string, excludedInput bool) string {
if input == "" {
return input
}

// Remove extra spaces
input = strings.Join(strings.Fields(input), " ")

// Replace spaces within quoted strings with '<->'
input = quotedStringsRegex.ReplaceAllStringFunc(input, func(match string) string {
return strings.Replace(match, " ", "<->", -1)
})

// Replace spaces outside of quoted substrings with '&' or '|'
replacer := "&"
if excludedInput || params.OrTerms {
replacer = "|"
}
input = strings.Replace(input, " ", replacer, -1)

return input
}

var termsClause string
if params.OrTerms {
termsClause = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
} else if strings.HasPrefix(terms, `"`) && strings.HasSuffix(terms, `"`) {
termsClause = "(" + strings.Join(strings.Fields(terms), " <-> ") + ")" + excludeClause
} else {
termsClause = "(" + strings.Join(strings.Fields(terms), " & ") + ")" + excludeClause
tsQueryClause := replaceSpaces(terms, false)
excludedClause := replaceSpaces(excludedTerms, true)
if excludedClause != "" {
tsQueryClause += " &!(" + excludedClause + ")"
}

searchClause := fmt.Sprintf("to_tsvector('%[1]s', %[2]s) @@ to_tsquery('%[1]s', ?)", s.pgDefaultTextSearchConfig, searchType)
baseQuery = baseQuery.Where(searchClause, termsClause)
baseQuery = baseQuery.Where(searchClause, tsQueryClause)
} else if s.DriverName() == model.DatabaseDriverMysql {
if searchType == "Message" {
terms, err = removeMysqlStopWordsFromTerms(terms)
Expand Down

0 comments on commit b245d96

Please sign in to comment.