internal/postgres: support 3 parallel multi-word searches

Symbol search now supports up to 3 parallel multi-word searches.

For example, previously "bee cmd command" would return 0 results, and
now it returns results for type Command in
github.com/beego/bee/cmd/commands.

A test is also added for the multiWordSearchCombinations function.

For golang/go#44142

Change-Id: I6cc6aa87655576c2d9c13dc90ea18d12d8b19c9c
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/342472
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/postgres/symbolsearch.go b/internal/postgres/symbolsearch.go
index 04b3fa1..9ae9800 100644
--- a/internal/postgres/symbolsearch.go
+++ b/internal/postgres/symbolsearch.go
@@ -229,13 +229,18 @@
 		}
 		// If it is, try search for this word assuming it is the symbol name
 		// and everything else is a path element.
-		symbolToPathTokens[w] = strings.Join(append(append([]string{}, words[0:i]...), words[i+1:]...), " & ")
+		pathTokens := append(append([]string{}, words[0:i]...), words[i+1:]...)
+		sort.Strings(pathTokens)
+		symbolToPathTokens[w] = strings.Join(pathTokens, " & ")
 	}
-	if len(symbolToPathTokens) > 2 {
-		// There are more than 2 possible searches that can be performed, so
-		// just perform an OR query.
-		orQuery := strings.Join(strings.Fields(q), " | ")
-		return map[string]string{orQuery: orQuery}
+	if len(symbolToPathTokens) == 0 {
+		return nil
+	}
+	if len(symbolToPathTokens) > 3 {
+		// There are too many searches that can be performed, so
+		// return no results.
+		// TODO(golang/go#44142): Leave add support for an OR query.
+		return nil
 	}
 	return symbolToPathTokens
 }
diff --git a/internal/postgres/symbolsearch_test.go b/internal/postgres/symbolsearch_test.go
index 0eb9004..2b9021f 100644
--- a/internal/postgres/symbolsearch_test.go
+++ b/internal/postgres/symbolsearch_test.go
@@ -136,3 +136,59 @@
 	m2.Packages()[0].Documentation[0].API = sample.API
 	MustInsertModule(ctx, t, testDB, m2)
 }
+
+func TestMultiwordSearchCombinations(t *testing.T) {
+	for _, test := range []struct {
+		q    string
+		want map[string]string
+	}{
+		{
+			q: "github.com foo",
+			want: map[string]string{
+				"foo": "github.com",
+			},
+		},
+		{
+			q: "julieqiu foo",
+			want: map[string]string{
+				"julieqiu": "foo",
+				"foo":      "julieqiu",
+			},
+		},
+		{
+			q: "github.com/julieqiu foo",
+			want: map[string]string{
+				"foo": "github.com/julieqiu",
+			},
+		},
+		{
+			q: "github.com julieqiu/api-demo foo",
+			want: map[string]string{
+				"foo": "github.com & julieqiu/api-demo",
+			},
+		},
+		{
+			q:    "github.com julieqiu/api-demo",
+			want: nil,
+		},
+		{
+			q: "bee cmd command",
+			want: map[string]string{
+				"bee":     "cmd & command",
+				"cmd":     "bee & command",
+				"command": "bee & cmd",
+			},
+		},
+		{
+			q:    "bee beego cmd command",
+			want: nil,
+		},
+	} {
+		t.Run(test.q, func(t *testing.T) {
+			got := multiwordSearchCombinations(test.q)
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}