Skip to content

Commit 1677a30

Browse files
authored
fix: add support for spaces in search & enable searching by display name in templates (#19552)
## Summary In this pull request we're updating search to support queries with spaces in addition to the `field:value` pattern that is currently supported. Additionally templates search now defaults to `display_name` (since `display_name` is optional the search will fallback to `name`) when searching without the `field:value` pattern Closes: #14384 ### Downsides with searching on `name` and `display_name` Because the `name` field cannot include spaces, we end up in a situation where including a space in the query will result in no results since the query searches on both `name` AND `display_name`. In the following example, we can see the results of searching by both `name` and `display_name` on these templates: | Name | Display Name | | ------ | ------------- | | docker | Docker Template | | faketemplate | A Fake Template | | azure | Fake Azure Template | | anotherfake | Another Fake Template | | azurefake | Another Fake Fake Azure Template | https://github.com/user-attachments/assets/b0e0793e-e77d-46bc-9a42-d7cf4f8bd910 ### Proposal: Search on `display_name` by default and allow for `name` using the `field:value` pattern If we remove `name` from the default template search, we're now able to search with spaces on template `display_names`. Since `display_names` are what users see in the templates list they might expect the search to work this way. Below is an example of `name` being removed from the default template search. https://github.com/user-attachments/assets/9aba5911-4960-4384-befb-08ea1acaa3ab With this approach users would still be able to search on template names by specifying `exact_name:foo`. ### Testing Added additional test cases to ensure spaces were handled as expected in combination with `field:value` patterns.
1 parent ff18499 commit 1677a30

File tree

5 files changed

+121
-18
lines changed

5 files changed

+121
-18
lines changed

coderd/database/modelqueries.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
7878
arg.Deleted,
7979
arg.OrganizationID,
8080
arg.ExactName,
81+
arg.ExactDisplayName,
8182
arg.FuzzyName,
83+
arg.FuzzyDisplayName,
8284
pq.Array(arg.IDs),
8385
arg.Deprecated,
8486
arg.HasAITask,

coderd/database/queries.sql.go

Lines changed: 36 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templates.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,30 @@ WHERE
3030
LOWER(t.name) = LOWER(@exact_name)
3131
ELSE true
3232
END
33+
-- Filter by exact display name
34+
AND CASE
35+
WHEN @exact_display_name :: text != '' THEN
36+
LOWER(t.display_name) = LOWER(@exact_display_name)
37+
ELSE true
38+
END
3339
-- Filter by name, matching on substring
3440
AND CASE
3541
WHEN @fuzzy_name :: text != '' THEN
3642
lower(t.name) ILIKE '%' || lower(@fuzzy_name) || '%'
3743
ELSE true
3844
END
45+
-- Filter by display_name, matching on substring (fallback to name if display_name is empty)
46+
AND CASE
47+
WHEN @fuzzy_display_name :: text != '' THEN
48+
CASE
49+
WHEN t.display_name IS NOT NULL AND t.display_name != '' THEN
50+
lower(t.display_name) ILIKE '%' || lower(@fuzzy_display_name) || '%'
51+
ELSE
52+
-- Remove spaces if present since 't.name' cannot have any spaces
53+
lower(t.name) ILIKE '%' || REPLACE(lower(@fuzzy_display_name), ' ', '') || '%'
54+
END
55+
ELSE true
56+
END
3957
-- Filter by ids
4058
AND CASE
4159
WHEN array_length(@ids :: uuid[], 1) > 0 THEN

coderd/searchquery/search.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ func Templates(ctx context.Context, db database.Store, actorID uuid.UUID, query
268268
// Always lowercase for all searches.
269269
query = strings.ToLower(query)
270270
values, errors := searchTerms(query, func(term string, values url.Values) error {
271-
// Default to the template name
272-
values.Add("name", term)
271+
// Default to the display name
272+
values.Add("display_name", term)
273273
return nil
274274
})
275275
if len(errors) > 0 {
@@ -281,7 +281,9 @@ func Templates(ctx context.Context, db database.Store, actorID uuid.UUID, query
281281
Deleted: parser.Boolean(values, false, "deleted"),
282282
OrganizationID: parseOrganization(ctx, db, parser, values, "organization"),
283283
ExactName: parser.String(values, "", "exact_name"),
284+
ExactDisplayName: parser.String(values, "", "exact_display_name"),
284285
FuzzyName: parser.String(values, "", "name"),
286+
FuzzyDisplayName: parser.String(values, "", "display_name"),
285287
IDs: parser.UUIDs(values, []uuid.UUID{}, "ids"),
286288
Deprecated: parser.NullableBoolean(values, sql.NullBool{}, "deprecated"),
287289
HasAITask: parser.NullableBoolean(values, sql.NullBool{}, "has-ai-task"),
@@ -305,7 +307,8 @@ func searchTerms(query string, defaultKey func(term string, values url.Values) e
305307
// Because we do this in 2 passes, we want to maintain quotes on the first
306308
// pass. Further splitting occurs on the second pass and quotes will be
307309
// dropped.
308-
elements := splitQueryParameterByDelimiter(query, ' ', true)
310+
tokens := splitQueryParameterByDelimiter(query, ' ', true)
311+
elements := processTokens(tokens)
309312
for _, element := range elements {
310313
if strings.HasPrefix(element, ":") || strings.HasSuffix(element, ":") {
311314
return nil, []codersdk.ValidationError{
@@ -385,3 +388,24 @@ func splitQueryParameterByDelimiter(query string, delimiter rune, maintainQuotes
385388

386389
return parts
387390
}
391+
392+
// processTokens takes the split tokens and groups them based on a delimiter (':').
393+
// Tokens without a delimiter present are joined to support searching with spaces.
394+
//
395+
// Example Input: ['deprecated:false', 'test', 'template']
396+
// Example Output: ['deprecated:false', 'test template']
397+
func processTokens(tokens []string) []string {
398+
var results []string
399+
var nonFieldTerms []string
400+
for _, token := range tokens {
401+
if strings.Contains(token, string(':')) {
402+
results = append(results, token)
403+
} else {
404+
nonFieldTerms = append(nonFieldTerms, token)
405+
}
406+
}
407+
if len(nonFieldTerms) > 0 {
408+
results = append(results, strings.Join(nonFieldTerms, " "))
409+
}
410+
return results
411+
}

coderd/searchquery/search_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ func TestSearchTemplates(t *testing.T) {
686686
Name: "OnlyName",
687687
Query: "foobar",
688688
Expected: database.GetTemplatesWithFilterParams{
689-
FuzzyName: "foobar",
689+
FuzzyDisplayName: "foobar",
690690
},
691691
},
692692
{
@@ -757,6 +757,43 @@ func TestSearchTemplates(t *testing.T) {
757757
AuthorID: userID,
758758
},
759759
},
760+
{
761+
Name: "SearchOnDisplayName",
762+
Query: "test name",
763+
Expected: database.GetTemplatesWithFilterParams{
764+
FuzzyDisplayName: "test name",
765+
},
766+
},
767+
{
768+
Name: "NameField",
769+
Query: "name:testname",
770+
Expected: database.GetTemplatesWithFilterParams{
771+
FuzzyName: "testname",
772+
},
773+
},
774+
{
775+
Name: "QuotedValue",
776+
Query: `name:"test name"`,
777+
Expected: database.GetTemplatesWithFilterParams{
778+
FuzzyName: "test name",
779+
},
780+
},
781+
{
782+
Name: "MultipleTerms",
783+
Query: `foo bar exact_name:"test display name"`,
784+
Expected: database.GetTemplatesWithFilterParams{
785+
ExactName: "test display name",
786+
FuzzyDisplayName: "foo bar",
787+
},
788+
},
789+
{
790+
Name: "FieldAndSpaces",
791+
Query: "deprecated:false test template",
792+
Expected: database.GetTemplatesWithFilterParams{
793+
Deprecated: sql.NullBool{Bool: false, Valid: true},
794+
FuzzyDisplayName: "test template",
795+
},
796+
},
760797
}
761798

762799
for _, c := range testCases {

0 commit comments

Comments
 (0)