MM-19334 Ported AutocompleteInTeamForSearch to Squirrel (#19723)

Automatic Merge
Этот коммит содержится в:
Tim Scheuermann
2022-03-10 20:14:19 +01:00
коммит произвёл GitHub
родитель 7664c9d709
Коммит f5b6e09965
2 изменённых файлов: 191 добавлений и 89 удалений

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

@@ -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{}