internal/related: group suggested content

Currently, all types of content (issues, changes, discussions, blogs,
etc.) are grouped together. Since the related post only visualizes the
content title, it is hard for the user to figure out the content types.

This CL groups the contents and presents them separately within a single
post in this specific order: issues, changes, documentation, and
discussions.

Fixes golang/oscar#41

Change-Id: I8b167d96852ca561a6a5f05f935a62d488fd6081
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/625575
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/internal/related/related.go b/internal/related/related.go
index f95fb5c..8666425 100644
--- a/internal/related/related.go
+++ b/internal/related/related.go
@@ -371,27 +371,84 @@
 	return results, true
 }
 
+// relatedContentGroup is used to represent different
+// groupings of the related post content. Examples
+// are groups containing related issues and group
+// with related code changes.
+type relatedContentGroup float64
+
+const (
+	issues relatedContentGroup = iota
+	changes
+	discussions
+	documentation
+)
+
+// relatedGroupTitles are the titles for each
+// related content group, to be displayed in
+// in the related post comment.
+var relatedGroupTitles = map[relatedContentGroup]string{
+	issues:        "Related Issues",
+	changes:       "Related Code Changes",
+	discussions:   "Related Discussions",
+	documentation: "Related Documentation",
+}
+
 // comment returns the comment to post to GitHub for the given related
 // issues.
 func (p *Poster) comment(results []search.Result) string {
-	var comment strings.Builder
-	fmt.Fprintf(&comment, "**Related Issues and Documentation**\n\n")
+	// Break results into issues, changes, discusssions
+	// and documentation sections.
+	rg := make(map[relatedContentGroup][]search.Result)
 	for _, r := range results {
-		title := cleanTitle(r.ID)
-		if r.Title != "" {
-			title = r.Title
+		switch r.Kind {
+		case search.KindGitHubIssue:
+			rg[issues] = append(rg[issues], r)
+		case search.KindGoGerritChange:
+			rg[changes] = append(rg[changes], r)
+		case search.KindGitHubDiscussion, search.KindGoogleGroupConversation:
+			rg[discussions] = append(rg[discussions], r)
+		default:
+			// KindGoDocumentation, KindGoDevPage, KindGoWiki,
+			// KindGoBlog, KindGoReference
+			rg[documentation] = append(rg[documentation], r)
 		}
-		info := ""
-		if issue, err := p.github.LookupIssueURL(r.ID); err == nil {
-			info = fmt.Sprint(" #", issue.Number)
-			if issue.ClosedAt != "" {
-				info += " (closed)"
-			}
-		}
-		fmt.Fprintf(&comment, " - [%s%s](%s) <!-- score=%.5f -->\n", markdownEscape(title), info, r.ID, r.Score)
 	}
-	fmt.Fprintf(&comment, "\n<sub>(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in [this discussion](https://github.com/golang/go/discussions/67901).)</sub>\n")
-	return comment.String()
+
+	// section generates a comment markdown for a group
+	// of results with a title.
+	section := func(title string, results []search.Result) string {
+		var comment strings.Builder
+		fmt.Fprintf(&comment, "**%s**\n\n", title)
+		for _, r := range results {
+			title := cleanTitle(r.ID)
+			if r.Title != "" {
+				title = r.Title
+			}
+			info := ""
+			if issue, err := p.github.LookupIssueURL(r.ID); err == nil {
+				info = fmt.Sprint(" #", issue.Number)
+				if issue.ClosedAt != "" {
+					info += " (closed)"
+				}
+			}
+			fmt.Fprintf(&comment, " - [%s%s](%s) <!-- score=%.5f -->\n", markdownEscape(title), info, r.ID, r.Score)
+		}
+		return comment.String()
+	}
+
+	var sections []string
+	for _, group := range []relatedContentGroup{issues, changes, documentation, discussions} {
+		res := rg[group]
+		if len(res) == 0 {
+			continue
+		}
+		s := section(relatedGroupTitles[group], res)
+		sections = append(sections, s)
+	}
+
+	footer := "\n<sub>(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in [this discussion](https://github.com/golang/go/discussions/67901).)</sub>\n"
+	return strings.Join(sections, "\n") + footer
 }
 
 // cleanTitle cleans up document title t to make it more readable
diff --git a/internal/related/related_test.go b/internal/related/related_test.go
index 946fce1..65856e3 100644
--- a/internal/related/related_test.go
+++ b/internal/related/related_test.go
@@ -22,6 +22,7 @@
 	"golang.org/x/oscar/internal/embeddocs"
 	"golang.org/x/oscar/internal/github"
 	"golang.org/x/oscar/internal/llm"
+	"golang.org/x/oscar/internal/search"
 	"golang.org/x/oscar/internal/storage"
 	"golang.org/x/oscar/internal/testutil"
 )
@@ -207,6 +208,83 @@
 	})
 }
 
+func TestPostComment(t *testing.T) {
+	lg := testutil.Slogger(t)
+	db := storage.MemDB()
+	gh := github.New(lg, db, nil, nil)
+	p := New(lg, db, gh, nil, nil, t.Name())
+
+	results := []search.Result{
+		{
+			Kind:         search.KindGitHubIssue,
+			VectorResult: storage.VectorResult{ID: "https://github.com/rsc/markdown/issues/1"},
+			Title:        "Support Github Emojis",
+		},
+		{
+			Kind:         search.KindGitHubDiscussion,
+			VectorResult: storage.VectorResult{ID: "https://github.com/golang/go/discussions/67901"},
+			Title:        "gabyhelp feedback",
+		},
+		{
+			Kind:         search.KindGoBlog,
+			VectorResult: storage.VectorResult{ID: "https://go.dev/blog/govulncheck"},
+			Title:        "Govulncheck v1.0.0 is released!",
+		},
+		{
+			Kind:         search.KindGoGerritChange,
+			VectorResult: storage.VectorResult{ID: "https://go-review.googlesource.com/c/test/+/1#related-content"},
+			Title:        "all: update dependencies",
+		},
+		{
+			Kind:         search.KindGoogleGroupConversation,
+			VectorResult: storage.VectorResult{ID: "https://groups.google.com/g/golang-nuts/c/MKgGqer_taI"},
+			Title:        "Returning a pointer or value struct.",
+		},
+		{
+			Kind:         search.KindGitHubIssue,
+			VectorResult: storage.VectorResult{ID: "https://github.com/rsc/markdown/issues/2"},
+			Title:        "allow capital X in task list items",
+		},
+		{
+			Kind:         search.KindGoWiki,
+			VectorResult: storage.VectorResult{ID: "https://go.dev/wiki/Iota"},
+			Title:        "Go Wiki: Iota",
+		},
+		{
+			Kind:         search.KindGoReference,
+			VectorResult: storage.VectorResult{ID: "https://go.dev/ref/spec"},
+			Title:        "The Go Programming Language Specification",
+		},
+	}
+
+	want := `**Related Issues**
+
+ - [Support Github Emojis](https://github.com/rsc/markdown/issues/1) <!-- score=0.00000 -->
+ - [allow capital X in task list items](https://github.com/rsc/markdown/issues/2) <!-- score=0.00000 -->
+
+**Related Code Changes**
+
+ - [all: update dependencies](https://go-review.googlesource.com/c/test/+/1#related-content) <!-- score=0.00000 -->
+
+**Related Documentation**
+
+ - [Govulncheck v1.0.0 is released!](https://go.dev/blog/govulncheck) <!-- score=0.00000 -->
+ - [Go Wiki: Iota](https://go.dev/wiki/Iota) <!-- score=0.00000 -->
+ - [The Go Programming Language Specification](https://go.dev/ref/spec) <!-- score=0.00000 -->
+
+**Related Discussions**
+
+ - [gabyhelp feedback](https://github.com/golang/go/discussions/67901) <!-- score=0.00000 -->
+ - [Returning a pointer or value struct.](https://groups.google.com/g/golang-nuts/c/MKgGqer_taI) <!-- score=0.00000 -->
+
+<sub>(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in [this discussion](https://github.com/golang/go/discussions/67901).)</sub>
+`
+
+	if got := p.comment(results); want != got {
+		t.Errorf("want %s comment; got %s", want, got)
+	}
+}
+
 func newTestPoster(t *testing.T) (_ *Poster, out *bytes.Buffer, project string, check func(err error)) {
 	t.Helper()
 
@@ -279,7 +357,7 @@
 	return time.Time{}
 }
 
-var post13 = unQUOT(`**Related Issues and Documentation**
+var post13 = unQUOT(`**Related Issues**
 
  - [goldmark and markdown diff with h1 inside p #6 (closed)](https://github.com/rsc/markdown/issues/6) <!-- score=0.92657 -->
  - [Support escaped \QUOT|\QUOT in table cells #9 (closed)](https://github.com/rsc/markdown/issues/9) <!-- score=0.91858 -->
@@ -295,7 +373,7 @@
 <sub>(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in [this discussion](https://github.com/golang/go/discussions/67901).)</sub>
 `)
 
-var post19 = unQUOT(`**Related Issues and Documentation**
+var post19 = unQUOT(`**Related Issues**
 
  - [allow capital X in task list items #2 (closed)](https://github.com/rsc/markdown/issues/2) <!-- score=0.92943 -->
  - [Support escaped \QUOT|\QUOT in table cells #9 (closed)](https://github.com/rsc/markdown/issues/9) <!-- score=0.91994 -->
diff --git a/internal/search/search.go b/internal/search/search.go
index c674120..0aafc9b 100644
--- a/internal/search/search.go
+++ b/internal/search/search.go
@@ -272,8 +272,8 @@
 		return false
 	}
 
-	// Group must be "golang-", except in tests.
-	if !strings.HasPrefix(s[1], "golang-") && !testing.Testing() {
+	// Group must be "golang-*".
+	if !strings.HasPrefix(s[1], "golang-") {
 		return false
 	}
 	return true