MM-19334 Ported AutocompleteInTeamForSearch to Squirrel (#19723)
Automatic Merge
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7664c9d709
Коммит
f5b6e09965
@@ -2850,47 +2850,87 @@ func (s SqlChannelStore) AutocompleteInTeam(teamID, userID, term string, include
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rewrite in squirrel (https://github.com/mattermost/mattermost-server/issues/19334)
|
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamID string, userID string, term string, includeDeleted bool) (model.ChannelList, error) {
|
||||||
func (s SqlChannelStore) AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) (model.ChannelList, error) {
|
// shared query
|
||||||
deleteFilter := "AND DeleteAt = 0"
|
query := s.getSubQueryBuilder().Select("C.*").
|
||||||
if includeDeleted {
|
From("Channels AS C").
|
||||||
deleteFilter = ""
|
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
|
||||||
|
Limit(50).
|
||||||
|
Where(sq.And{
|
||||||
|
sq.Or{
|
||||||
|
sq.Eq{"C.TeamId": teamID},
|
||||||
|
sq.Eq{
|
||||||
|
"C.TeamId": "",
|
||||||
|
"C.Type": model.ChannelTypeGroup,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sq.Eq{"CM.UserId": userID},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !includeDeleted {
|
||||||
|
// include the DeleteAt = 0 condition
|
||||||
|
query.Where(sq.Eq{"DeleteAt": 0})
|
||||||
}
|
}
|
||||||
|
|
||||||
queryFormat := `
|
var (
|
||||||
SELECT
|
channels = model.ChannelList{}
|
||||||
C.*
|
sql string
|
||||||
FROM
|
args []interface{}
|
||||||
Channels AS C
|
)
|
||||||
JOIN
|
|
||||||
ChannelMembers AS CM ON CM.ChannelId = C.Id
|
|
||||||
WHERE
|
|
||||||
(C.TeamId = :TeamId OR (C.TeamId = '' AND C.Type = :ChannelType))
|
|
||||||
AND CM.UserId = :UserId
|
|
||||||
` + deleteFilter + `
|
|
||||||
%v
|
|
||||||
LIMIT 50`
|
|
||||||
|
|
||||||
var channels model.ChannelList
|
// build the like clause
|
||||||
|
like := s.buildLIKEClauseX(term, "Name", "DisplayName", "Purpose")
|
||||||
|
if like == nil {
|
||||||
|
var err error
|
||||||
|
|
||||||
if likeClause, likeTerm := s.buildLIKEClause(term, "Name, DisplayName, Purpose"); likeClause == "" {
|
// generate the SQL query
|
||||||
if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"TeamId": teamId, "UserId": userId, "ChannelType": model.ChannelTypeGroup}); err != nil {
|
sql, args, err = query.ToSql()
|
||||||
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Tosql")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// build the full text search clause
|
||||||
|
full := s.buildFulltextClauseX(term, "Name", "DisplayName", "Purpose")
|
||||||
|
if full == nil {
|
||||||
|
return nil, errors.New("failed to build full text query for term=" + term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the LIKE query
|
||||||
|
likeSQL, likeArgs, err := query.Where(like).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Like_Tosql")
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the full text query
|
||||||
|
fullSQL, fullArgs, err := query.Where(full).ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Full_Tosql")
|
||||||
|
}
|
||||||
|
|
||||||
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
|
// Using a UNION results in index_merge and fulltext queries and is much faster than the ref
|
||||||
// query you would get using an OR of the LIKE and full-text clauses.
|
// query you would get using an OR of the LIKE and full-text clauses.
|
||||||
fulltextClause, fulltextTerm := s.buildFulltextClause(term, "Name, DisplayName, Purpose")
|
sql = fmt.Sprintf("(%s) UNION (%s) LIMIT 50", likeSQL, fullSQL)
|
||||||
likeQuery := fmt.Sprintf(queryFormat, "AND "+likeClause)
|
args = append(likeArgs, fullArgs...)
|
||||||
fulltextQuery := fmt.Sprintf(queryFormat, "AND "+fulltextClause)
|
}
|
||||||
query := fmt.Sprintf("(%v) UNION (%v) LIMIT 50", likeQuery, fulltextQuery)
|
|
||||||
|
|
||||||
if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"TeamId": teamId, "UserId": userId, "LikeTerm": likeTerm, "FulltextTerm": fulltextTerm, "ChannelType": model.ChannelTypeGroup}); err != nil {
|
var err error
|
||||||
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
|
|
||||||
|
// since the UNION is not part of squirrel, we need to assemble it and then update
|
||||||
|
// the placeholders manually
|
||||||
|
if s.DriverName() == model.DatabaseDriverPostgres {
|
||||||
|
sql, err = sq.Dollar.ReplacePlaceholders(sql)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "AutocompleteInTeamForSearch_Placeholder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userId, term)
|
// query the database
|
||||||
|
err = s.GetReplicaX().Select(&channels, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
|
||||||
|
}
|
||||||
|
|
||||||
|
directChannels, err := s.autocompleteInTeamForSearchDirectMessages(userID, term)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -2900,47 +2940,51 @@ func (s SqlChannelStore) AutocompleteInTeamForSearch(teamId string, userId strin
|
|||||||
sort.Slice(channels, func(a, b int) bool {
|
sort.Slice(channels, func(a, b int) bool {
|
||||||
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
|
return strings.ToLower(channels[a].DisplayName) < strings.ToLower(channels[b].DisplayName)
|
||||||
})
|
})
|
||||||
|
|
||||||
return channels, nil
|
return channels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rewrite in squirrel (https://github.com/mattermost/mattermost-server/issues/19334)
|
func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userID string, term string) ([]*model.Channel, error) {
|
||||||
func (s SqlChannelStore) autocompleteInTeamForSearchDirectMessages(userId string, term string) ([]*model.Channel, error) {
|
// create the main query
|
||||||
queryFormat := `
|
query := s.getQueryBuilder().Select("C.*", "OtherUsers.Username as DisplayName").
|
||||||
SELECT
|
From("Channels AS C").
|
||||||
C.*,
|
Join("ChannelMembers AS CM ON CM.ChannelId = C.Id").
|
||||||
OtherUsers.Username as DisplayName
|
Where(sq.Eq{
|
||||||
FROM
|
"C.Type": model.ChannelTypeDirect,
|
||||||
Channels AS C
|
"CM.UserId": userID,
|
||||||
JOIN
|
}).
|
||||||
ChannelMembers AS CM ON CM.ChannelId = C.Id
|
Limit(50)
|
||||||
INNER JOIN (
|
|
||||||
SELECT
|
|
||||||
ICM.ChannelId AS ChannelId, IU.Username AS Username
|
|
||||||
FROM
|
|
||||||
Users as IU
|
|
||||||
JOIN
|
|
||||||
ChannelMembers AS ICM ON ICM.UserId = IU.Id
|
|
||||||
WHERE
|
|
||||||
IU.Id != :UserId
|
|
||||||
%v
|
|
||||||
) AS OtherUsers ON OtherUsers.ChannelId = C.Id
|
|
||||||
WHERE
|
|
||||||
C.Type = :ChannelType
|
|
||||||
AND CM.UserId = :UserId
|
|
||||||
LIMIT 50`
|
|
||||||
|
|
||||||
var channels model.ChannelList
|
// create the subquery
|
||||||
|
subQuery := s.getSubQueryBuilder().Select("ICM.ChannelId AS ChannelId", "IU.Username AS Username").
|
||||||
|
From("Users AS IU").
|
||||||
|
Join("ChannelMembers AS ICM ON ICM.UserId = IU.Id").
|
||||||
|
Where(sq.NotEq{"IU.Id": userID})
|
||||||
|
|
||||||
if likeClause, likeTerm := s.buildLIKEClause(term, "IU.Username, IU.Nickname"); likeClause == "" {
|
// try to create a LIKE clause from the search term
|
||||||
if _, err := s.GetReplica().Select(&channels, fmt.Sprintf(queryFormat, ""), map[string]interface{}{"UserId": userId, "ChannelType": model.ChannelTypeDirect}); err != nil {
|
if like := s.buildLIKEClauseX(term, "IU.Username", "IU.Nickname"); like != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
|
subQuery = subQuery.Where(like)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
query := fmt.Sprintf(queryFormat, "AND "+likeClause)
|
|
||||||
|
|
||||||
if _, err := s.GetReplica().Select(&channels, query, map[string]interface{}{"UserId": userId, "LikeTerm": likeTerm, "ChannelType": model.ChannelTypeDirect}); err != nil {
|
// put the subquery into an INNER JOIN
|
||||||
return nil, errors.Wrapf(err, "failed to find Channels with term='%s'", term)
|
innerJoin := subQuery.
|
||||||
}
|
Prefix("INNER JOIN (").
|
||||||
|
Suffix(") AS OtherUsers ON OtherUsers.ChannelId = C.Id")
|
||||||
|
|
||||||
|
// add the subquery to the main query
|
||||||
|
query = query.JoinClause(innerJoin)
|
||||||
|
|
||||||
|
// create the SQL query and argument list
|
||||||
|
sql, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "autocompleteInTeamForSearchDirectMessages_InnerJoin_Tosql")
|
||||||
|
}
|
||||||
|
|
||||||
|
// query the channel list from the database using SQLX
|
||||||
|
channels := model.ChannelList{}
|
||||||
|
err = s.GetReplicaX().Select(&channels, sql, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to find Channels with term='%s' (%s %% %v)", term, sql, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
return channels, nil
|
return channels, nil
|
||||||
@@ -3247,29 +3291,57 @@ func (s SqlChannelStore) buildLIKEClause(term string, searchColumns string) (lik
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) buildLIKEClauseX(term string, searchColumns ...string) sq.Sqlizer {
|
||||||
|
// escape the special characters with *
|
||||||
|
likeTerm := sanitizeSearchTerm(term, "*")
|
||||||
|
|
||||||
|
if likeTerm == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a placeholder at the beginning and end
|
||||||
|
likeTerm = wildcardSearchTerm(likeTerm)
|
||||||
|
|
||||||
|
// Prepare the LIKE portion of the query.
|
||||||
|
var searchFields sq.Or
|
||||||
|
|
||||||
|
for _, field := range searchColumns {
|
||||||
|
if s.DriverName() == model.DatabaseDriverPostgres {
|
||||||
|
expr := fmt.Sprintf("LOWER(%s) LIKE LOWER(?) ESCAPE '*'", field)
|
||||||
|
searchFields = append(searchFields, sq.Expr(expr, likeTerm))
|
||||||
|
} else {
|
||||||
|
expr := fmt.Sprintf("%s LIKE ? ESCAPE '*'", field)
|
||||||
|
searchFields = append(searchFields, sq.Expr(expr, likeTerm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchFields
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceFulltextSearchChars = "<>+-()~:*\"!@"
|
||||||
|
|
||||||
func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string) (fulltextClause, fulltextTerm string) {
|
func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string) (fulltextClause, fulltextTerm string) {
|
||||||
// Copy the terms as we will need to prepare them differently for each search type.
|
// Copy the terms as we will need to prepare them differently for each search type.
|
||||||
fulltextTerm = term
|
fulltextTerm = term
|
||||||
|
|
||||||
// These chars must be treated as spaces in the fulltext query.
|
// These chars must be treated as spaces in the fulltext query.
|
||||||
for _, c := range spaceFulltextSearchChar {
|
fulltextTerm = strings.Map(func(r rune) rune {
|
||||||
fulltextTerm = strings.Replace(fulltextTerm, c, " ", -1)
|
if strings.ContainsRune(spaceFulltextSearchChars, r) {
|
||||||
}
|
return ' '
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, fulltextTerm)
|
||||||
|
|
||||||
// Prepare the FULLTEXT portion of the query.
|
// Prepare the FULLTEXT portion of the query.
|
||||||
if s.DriverName() == model.DatabaseDriverPostgres {
|
if s.DriverName() == model.DatabaseDriverPostgres {
|
||||||
fulltextTerm = strings.Replace(fulltextTerm, "|", "", -1)
|
fulltextTerm = strings.ReplaceAll(fulltextTerm, "|", "")
|
||||||
|
|
||||||
splitTerm := strings.Fields(fulltextTerm)
|
splitTerm := strings.Fields(fulltextTerm)
|
||||||
for i, t := range strings.Fields(fulltextTerm) {
|
for i, t := range strings.Fields(fulltextTerm) {
|
||||||
if i == len(splitTerm)-1 {
|
splitTerm[i] = t + ":*"
|
||||||
splitTerm[i] = t + ":*"
|
|
||||||
} else {
|
|
||||||
splitTerm[i] = t + ":* &"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fulltextTerm = strings.Join(splitTerm, " ")
|
fulltextTerm = strings.Join(splitTerm, " & ")
|
||||||
|
|
||||||
fulltextClause = fmt.Sprintf("((to_tsvector('english', %s)) @@ to_tsquery('english', :FulltextTerm))", convertMySQLFullTextColumnsToPostgres(searchColumns))
|
fulltextClause = fmt.Sprintf("((to_tsvector('english', %s)) @@ to_tsquery('english', :FulltextTerm))", convertMySQLFullTextColumnsToPostgres(searchColumns))
|
||||||
} else if s.DriverName() == model.DatabaseDriverMysql {
|
} else if s.DriverName() == model.DatabaseDriverMysql {
|
||||||
@@ -3286,6 +3358,51 @@ func (s SqlChannelStore) buildFulltextClause(term string, searchColumns string)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) buildFulltextClauseX(term string, searchColumns ...string) sq.Sqlizer {
|
||||||
|
// Copy the terms as we will need to prepare them differently for each search type.
|
||||||
|
fulltextTerm := term
|
||||||
|
|
||||||
|
// These chars must be treated as spaces in the fulltext query.
|
||||||
|
fulltextTerm = strings.Map(func(r rune) rune {
|
||||||
|
if strings.ContainsRune(spaceFulltextSearchChars, r) {
|
||||||
|
return ' '
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, fulltextTerm)
|
||||||
|
|
||||||
|
// Prepare the FULLTEXT portion of the query.
|
||||||
|
if s.DriverName() == model.DatabaseDriverPostgres {
|
||||||
|
// remove all pipes |
|
||||||
|
fulltextTerm = strings.ReplaceAll(fulltextTerm, "|", "")
|
||||||
|
|
||||||
|
// split the search term and append :* to each part
|
||||||
|
splitTerm := strings.Fields(fulltextTerm)
|
||||||
|
for i, t := range splitTerm {
|
||||||
|
splitTerm[i] = t + ":*"
|
||||||
|
}
|
||||||
|
|
||||||
|
// join the search term with &
|
||||||
|
fulltextTerm = strings.Join(splitTerm, " & ")
|
||||||
|
|
||||||
|
expr := fmt.Sprintf("((to_tsvector('english', %s)) @@ to_tsquery('english', ?))", strings.Join(searchColumns, " || ' ' || "))
|
||||||
|
return sq.Expr(expr, fulltextTerm)
|
||||||
|
|
||||||
|
} else if s.DriverName() == model.DatabaseDriverMysql {
|
||||||
|
|
||||||
|
splitTerm := strings.Fields(fulltextTerm)
|
||||||
|
for i, t := range splitTerm {
|
||||||
|
splitTerm[i] = "+" + t + "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fulltextTerm = strings.Join(splitTerm, " ")
|
||||||
|
|
||||||
|
expr := fmt.Sprintf("MATCH(%s) AGAINST (? IN BOOLEAN MODE)", strings.Join(searchColumns, ", "))
|
||||||
|
return sq.Expr(expr, fulltextTerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) performSearch(searchQuery string, term string, parameters map[string]interface{}) (model.ChannelList, error) {
|
func (s SqlChannelStore) performSearch(searchQuery string, term string, parameters map[string]interface{}) (model.ChannelList, error) {
|
||||||
likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose")
|
likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose")
|
||||||
if likeTerm == "" {
|
if likeTerm == "" {
|
||||||
|
|||||||
@@ -1455,21 +1455,6 @@ func (us SqlUserStore) SearchNotInGroup(groupID string, term string, options *mo
|
|||||||
return us.performSearch(query, term, options)
|
return us.performSearch(query, term, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
var spaceFulltextSearchChar = []string{
|
|
||||||
"<",
|
|
||||||
">",
|
|
||||||
"+",
|
|
||||||
"-",
|
|
||||||
"(",
|
|
||||||
")",
|
|
||||||
"~",
|
|
||||||
":",
|
|
||||||
"*",
|
|
||||||
"\"",
|
|
||||||
"!",
|
|
||||||
"@",
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSearchQuery(query sq.SelectBuilder, terms []string, fields []string, isPostgreSQL bool) sq.SelectBuilder {
|
func generateSearchQuery(query sq.SelectBuilder, terms []string, fields []string, isPostgreSQL bool) sq.SelectBuilder {
|
||||||
for _, term := range terms {
|
for _, term := range terms {
|
||||||
searchFields := []string{}
|
searchFields := []string{}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user