Skip to content

Commit e21c9a4

Browse files
committed
Added search using qualifier[:=]value syntax.
1 parent b82a519 commit e21c9a4

File tree

2 files changed

+151
-10
lines changed

2 files changed

+151
-10
lines changed

commands/lib/search.go

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,141 @@ func LibrarySearch(ctx context.Context, req *rpc.LibrarySearchRequest) (*rpc.Lib
3838
return searchLibrary(req, lm), nil
3939
}
4040

41+
// MatcherTokensFromQueryString parses the query string into tokens of interest
42+
// for the qualifier-value pattern matching.
43+
func MatcherTokensFromQueryString(query string) []string {
44+
escaped := false
45+
quoted := false
46+
tokens := []string{}
47+
sb := &strings.Builder{}
48+
49+
for _, r := range query {
50+
// Short circuit the loop on backslash so that all other paths can clear
51+
// the escaped flag.
52+
if !escaped && r == '\\' {
53+
escaped = true
54+
continue
55+
}
56+
57+
if r == '"' {
58+
if !escaped {
59+
quoted = !quoted
60+
} else {
61+
sb.WriteRune(r)
62+
}
63+
} else if !quoted && r == ' ' {
64+
tokens = append(tokens, strings.ToLower(sb.String()))
65+
sb.Reset()
66+
} else {
67+
sb.WriteRune(r)
68+
}
69+
escaped = false
70+
}
71+
if sb.Len() > 0 {
72+
tokens = append(tokens, strings.ToLower(sb.String()))
73+
}
74+
75+
return tokens
76+
}
77+
78+
// DefaulLibraryMatchExtractor returns a string describing the library that
79+
// is used for the simple search.
80+
func DefaultLibraryMatchExtractor(lib *librariesindex.Library) string {
81+
res := lib.Name + " " +
82+
lib.Latest.Paragraph + " " +
83+
lib.Latest.Sentence + " " +
84+
lib.Latest.Author + " "
85+
for _, include := range lib.Latest.ProvidesIncludes {
86+
res += include + " "
87+
}
88+
return res
89+
}
90+
91+
// MatcherFromQueryString returns a closure that takes a library as a
92+
// parameter and returns true if the library matches the query.
93+
func MatcherFromQueryString(query string) func(*librariesindex.Library) bool {
94+
// A qv-query is one using <qualifier>[:=]<value> syntax.
95+
qvQuery := strings.Contains(query, ":") || strings.Contains(query, "=")
96+
97+
if !qvQuery {
98+
queryTerms := utils.SearchTermsFromQueryString(query)
99+
return func(lib *librariesindex.Library) bool {
100+
return utils.Match(DefaultLibraryMatchExtractor(lib), queryTerms)
101+
}
102+
}
103+
104+
joinedStrings := func(strs []string) string {
105+
return strings.Join(strs, " ")
106+
}
107+
108+
qualifiers := []struct {
109+
key string
110+
extractor func(*librariesindex.Library) string
111+
}{
112+
// The library name comes from the Library object.
113+
{"name", func(lib *librariesindex.Library) string { return lib.Name }},
114+
115+
// All other values come from the latest Release.
116+
{"architectures", func(lib *librariesindex.Library) string { return joinedStrings(lib.Latest.Architectures) }},
117+
{"author", func(lib *librariesindex.Library) string { return lib.Latest.Author }},
118+
{"category", func(lib *librariesindex.Library) string { return lib.Latest.Category }},
119+
{"dependencies", func(lib *librariesindex.Library) string {
120+
names := []string{}
121+
for _, dep := range lib.Latest.Dependencies {
122+
names = append(names, dep.GetName())
123+
}
124+
return joinedStrings(names)
125+
}},
126+
{"maintainer", func(lib *librariesindex.Library) string { return lib.Latest.Maintainer }},
127+
{"paragraph", func(lib *librariesindex.Library) string { return lib.Latest.Paragraph }},
128+
{"sentence", func(lib *librariesindex.Library) string { return lib.Latest.Sentence }},
129+
{"types", func(lib *librariesindex.Library) string { return joinedStrings(lib.Latest.Types) }},
130+
{"version", func(lib *librariesindex.Library) string { return lib.Latest.Version.String() }},
131+
{"website", func(lib *librariesindex.Library) string { return lib.Latest.Website }},
132+
}
133+
134+
queryTerms := MatcherTokensFromQueryString(query)
135+
136+
return func(lib *librariesindex.Library) bool {
137+
matched := true
138+
for _, term := range queryTerms {
139+
140+
// Flag indicating whether the search term matched a known qualifier
141+
knownQualifier := false
142+
143+
for _, q := range qualifiers {
144+
if strings.HasPrefix(term, q.key+":") {
145+
target := strings.TrimPrefix(term, q.key+":")
146+
matched = (matched && utils.Match(q.extractor(lib), []string{target}))
147+
knownQualifier = true
148+
break
149+
} else if strings.HasPrefix(term, q.key+"=") {
150+
target := strings.TrimPrefix(term, q.key+"=")
151+
matched = (matched && strings.ToLower(q.extractor(lib)) == target)
152+
knownQualifier = true
153+
break
154+
}
155+
}
156+
157+
if !knownQualifier {
158+
matched = (matched && utils.Match(DefaultLibraryMatchExtractor(lib), []string{term}))
159+
}
160+
}
161+
return matched
162+
}
163+
}
164+
41165
func searchLibrary(req *rpc.LibrarySearchRequest, lm *librariesmanager.LibrariesManager) *rpc.LibrarySearchResponse {
42166
res := []*rpc.SearchedLibrary{}
43167
query := req.GetSearchArgs()
44168
if query == "" {
45169
query = req.GetQuery()
46170
}
47-
queryTerms := utils.SearchTermsFromQueryString(query)
48171

49-
for _, lib := range lm.Index.Libraries {
50-
toTest := lib.Name + " " +
51-
lib.Latest.Paragraph + " " +
52-
lib.Latest.Sentence + " " +
53-
lib.Latest.Author + " "
54-
for _, include := range lib.Latest.ProvidesIncludes {
55-
toTest += include + " "
56-
}
172+
matcher := MatcherFromQueryString(query)
57173

58-
if utils.Match(toTest, queryTerms) {
174+
for _, lib := range lm.Index.Libraries {
175+
if matcher(lib) {
59176
res = append(res, indexLibraryToRPCSearchLibrary(lib, req.GetOmitReleasesDetails()))
60177
}
61178
}

commands/lib/search_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,27 @@ func TestSearchLibraryFields(t *testing.T) {
9494
require.Len(t, res, 19)
9595
require.Equal(t, "FlashStorage", res[0])
9696
}
97+
98+
func TestSearchLibraryWithQualifiers(t *testing.T) {
99+
lm := librariesmanager.NewLibraryManager(fullIndexPath, nil)
100+
lm.LoadIndex()
101+
102+
query := func(q string) []string {
103+
libs := []string{}
104+
for _, lib := range searchLibrary(&rpc.LibrarySearchRequest{SearchArgs: q}, lm).Libraries {
105+
libs = append(libs, lib.Name)
106+
}
107+
return libs
108+
}
109+
110+
res := query("name:FlashStorage")
111+
require.Len(t, res, 7)
112+
113+
res = query("name=FlashStorage")
114+
require.Len(t, res, 1)
115+
require.Equal(t, "FlashStorage", res[0])
116+
117+
res = query("name=\"Painless Mesh\"")
118+
require.Len(t, res, 1)
119+
require.Equal(t, "Painless Mesh", res[0])
120+
}

0 commit comments

Comments
 (0)