Skip to content

Commit

Permalink
api: make fuzzy searching case-agnostic
Browse files Browse the repository at this point in the history
  • Loading branch information
shoenig committed Apr 16, 2021
1 parent 350d9eb commit ab92667
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 10 deletions.
32 changes: 22 additions & 10 deletions nomad/search_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ func (s *Search) getFuzzyMatches(iter memdb.ResultIterator, text string) (map[st
return m, truncations
}

// fuzzyIndex returns the index of text in name, ignoring case.
// text is assumed to be lower case.
// -1 is returned if name does not contain text.
func fuzzyIndex(name, text string) int {
lower := strings.ToLower(name)
return strings.Index(lower, text)
}

// fuzzySingleMatch determines if the ID of raw is a fuzzy match with text.
// Returns the context and score or nil if there is no match.
func (s *Search) fuzzyMatchSingle(raw interface{}, text string) (structs.Context, *fuzzyMatch) {
Expand All @@ -208,7 +216,7 @@ func (s *Search) fuzzyMatchSingle(raw interface{}, text string) (structs.Context
ctx = structs.Plugins
}

if idx := strings.Index(name, text); idx >= 0 {
if idx := fuzzyIndex(name, text); idx >= 0 {
return ctx, &fuzzyMatch{
id: name,
score: idx,
Expand All @@ -235,32 +243,32 @@ func (*Search) fuzzyMatchesJob(j *structs.Job, text string) map[structs.Context]
job := j.ID

// job.name
if idx := strings.Index(j.Name, text); idx >= 0 {
if idx := fuzzyIndex(j.Name, text); idx >= 0 {
sm[structs.Jobs] = append(sm[structs.Jobs], score(job, ns, idx))
}

// job|group.name
for _, group := range j.TaskGroups {
if idx := strings.Index(group.Name, text); idx >= 0 {
if idx := fuzzyIndex(group.Name, text); idx >= 0 {
sm[structs.Groups] = append(sm[structs.Groups], score(group.Name, ns, idx, job))
}

// job|group|service.name
for _, service := range group.Services {
if idx := strings.Index(service.Name, text); idx >= 0 {
if idx := fuzzyIndex(service.Name, text); idx >= 0 {
sm[structs.Services] = append(sm[structs.Services], score(service.Name, ns, idx, job, group.Name))
}
}

// job|group|task.name
for _, task := range group.Tasks {
if idx := strings.Index(task.Name, text); idx >= 0 {
if idx := fuzzyIndex(task.Name, text); idx >= 0 {
sm[structs.Tasks] = append(sm[structs.Tasks], score(task.Name, ns, idx, job, group.Name))
}

// job|group|task|service.name
for _, service := range task.Services {
if idx := strings.Index(service.Name, text); idx >= 0 {
if idx := fuzzyIndex(service.Name, text); idx >= 0 {
sm[structs.Services] = append(sm[structs.Services], score(service.Name, ns, idx, job, group.Name, task.Name))
}
}
Expand All @@ -269,17 +277,17 @@ func (*Search) fuzzyMatchesJob(j *structs.Job, text string) map[structs.Context]
switch task.Driver {
case "docker":
image := getConfigParam(task.Config, "image")
if idx := strings.Index(image, text); idx >= 0 {
if idx := fuzzyIndex(image, text); idx >= 0 {
sm[structs.Images] = append(sm[structs.Images], score(image, ns, idx, job, group.Name, task.Name))
}
case "exec", "raw_exec":
command := getConfigParam(task.Config, "command")
if idx := strings.Index(command, text); idx >= 0 {
if idx := fuzzyIndex(command, text); idx >= 0 {
sm[structs.Commands] = append(sm[structs.Commands], score(command, ns, idx, job, group.Name, task.Name))
}
case "java":
class := getConfigParam(task.Config, "class")
if idx := strings.Index(class, text); idx >= 0 {
if idx := fuzzyIndex(class, text); idx >= 0 {
sm[structs.Classes] = append(sm[structs.Classes], score(class, ns, idx, job, group.Name, task.Name))
}
}
Expand Down Expand Up @@ -621,6 +629,10 @@ func (s *Search) FuzzySearch(args *structs.FuzzySearchRequest, reply *structs.Fu
return fmt.Errorf("fuzzy search query must be at least %d characters, got %d", min, n)
}

// for case-insensitive searching, lower-case the search term once and reuse
text := strings.ToLower(args.Text)

// accumulate fuzzy search results and any truncations
reply.Matches = make(map[structs.Context][]structs.FuzzyMatch)
reply.Truncations = make(map[structs.Context]bool)

Expand Down Expand Up @@ -685,7 +697,7 @@ func (s *Search) FuzzySearch(args *structs.FuzzySearchRequest, reply *structs.Fu
// the response for negative results
reply.Truncations[iterCtx] = false

matches, truncations := s.getFuzzyMatches(iter, args.Text)
matches, truncations := s.getFuzzyMatches(iter, text)
for ctx := range matches {
reply.Matches[ctx] = matches[ctx]
}
Expand Down
45 changes: 45 additions & 0 deletions nomad/search_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,37 @@ func TestSearch_FuzzySearch_Namespace(t *testing.T) {
require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_FuzzySearch_Namespace_caseInsensitive(t *testing.T) {
t.Parallel()

s, cleanup := TestServer(t, func(c *Config) {
c.NumSchedulers = 0
})
defer cleanup()
codec := rpcClient(t, s)
testutil.WaitForLeader(t, s.RPC)

ns := mock.Namespace()
ns.Name = "TheFooNamespace"
require.NoError(t, s.fsm.State().UpsertNamespaces(2000, []*structs.Namespace{ns}))

req := &structs.FuzzySearchRequest{
Text: "foon",
Context: structs.Namespaces,
QueryOptions: structs.QueryOptions{
Region: "global",
},
}

var resp structs.FuzzySearchResponse
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Search.FuzzySearch", req, &resp))

require.Len(t, resp.Matches[structs.Namespaces], 1)
require.Equal(t, ns.Name, resp.Matches[structs.Namespaces][0].ID)
require.False(t, resp.Truncations[structs.Namespaces])
require.Equal(t, uint64(2000), resp.Index)
}

func TestSearch_FuzzySearch_ScalingPolicy(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -1992,3 +2023,17 @@ func TestSearch_FuzzySearch_Job(t *testing.T) {
}}, m[structs.Classes])
})
}

func TestSearch_FuzzySearch_fuzzyIndex(t *testing.T) {
for _, tc := range []struct {
name, text string
exp int
}{
{name: "foo-bar-baz", text: "bar", exp: 4},
{name: "Foo-Bar-Baz", text: "bar", exp: 4},
{name: "foo-bar-baz", text: "zap", exp: -1},
} {
result := fuzzyIndex(tc.name, tc.text)
require.Equal(t, tc.exp, result, "name: %s, text: %s, exp: %d, got: %d", tc.name, tc.text, tc.exp, result)
}
}

0 comments on commit ab92667

Please sign in to comment.