Skip to content

Commit a014d07

Browse files
GiteaBotlunnywxiaoguang
authored
Rework suggestion backend (#33538) (#33546)
Backport #33538 by @lunny Fix #33522 The suggestion backend logic now is - If the keyword is empty, returned the latest 5 issues/prs with index desc order - If the keyword is digital, find all issues/prs which `index` has a prefix with that, with index asc order - If the keyword is non-digital or if the queried records less than 5, searching issues/prs title with a `like`, with index desc order ## Empty keyword <img width="310" alt="image" src="https://github.com/user-attachments/assets/1912c634-0d98-4eeb-8542-d54240901f77" /> ## Digital <img width="479" alt="image" src="https://github.com/user-attachments/assets/0356a936-7110-4a24-b21e-7400201bf9b8" /> ## Digital and title contains the digital <img width="363" alt="image" src="https://github.com/user-attachments/assets/6e12f908-28fe-48de-8ccc-09cbeab024d4" /> ## non-Digital <img width="435" alt="image" src="https://github.com/user-attachments/assets/2722bb53-baa2-4d67-a224-522a65f73856" /> <img width="477" alt="image" src="https://github.com/user-attachments/assets/06708dd9-80d1-4a88-b32b-d29072dd1ba6" /> Co-authored-by: Lunny Xiao <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 312565e commit a014d07

File tree

4 files changed

+173
-49
lines changed

4 files changed

+173
-49
lines changed

Diff for: models/issues/issue.go

+40
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
user_model "code.gitea.io/gitea/models/user"
1818
"code.gitea.io/gitea/modules/container"
1919
"code.gitea.io/gitea/modules/log"
20+
"code.gitea.io/gitea/modules/optional"
2021
"code.gitea.io/gitea/modules/setting"
2122
api "code.gitea.io/gitea/modules/structs"
2223
"code.gitea.io/gitea/modules/timeutil"
@@ -531,6 +532,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
531532
return issue, nil
532533
}
533534

535+
func isPullToCond(isPull optional.Option[bool]) builder.Cond {
536+
if isPull.Has() {
537+
return builder.Eq{"is_pull": isPull.Value()}
538+
}
539+
return builder.NewCond()
540+
}
541+
542+
func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
543+
issues := make([]*Issue, 0, pageSize)
544+
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
545+
And(isPullToCond(isPull)).
546+
OrderBy("updated_unix DESC").
547+
Limit(pageSize).
548+
Find(&issues)
549+
return issues, err
550+
}
551+
552+
func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
553+
cond := builder.NewCond()
554+
if excludedID > 0 {
555+
cond = cond.And(builder.Neq{"`id`": excludedID})
556+
}
557+
558+
// It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
559+
// The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content"
560+
// But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
561+
// So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
562+
cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
563+
564+
issues := make([]*Issue, 0, pageSize)
565+
err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
566+
And(isPullToCond(isPull)).
567+
And(cond).
568+
OrderBy("updated_unix DESC, `index` DESC").
569+
Limit(pageSize).
570+
Find(&issues)
571+
return issues, err
572+
}
573+
534574
// GetIssueWithAttrsByIndex returns issue by index in a repository.
535575
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
536576
issue, err := GetIssueByIndex(ctx, repoID, index)

Diff for: routers/web/repo/issue_suggestions.go

+3-49
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@ package repo
66
import (
77
"net/http"
88

9-
"code.gitea.io/gitea/models/db"
10-
issues_model "code.gitea.io/gitea/models/issues"
119
"code.gitea.io/gitea/models/unit"
12-
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
1310
"code.gitea.io/gitea/modules/optional"
14-
"code.gitea.io/gitea/modules/structs"
1511
"code.gitea.io/gitea/services/context"
12+
issue_service "code.gitea.io/gitea/services/issue"
1613
)
1714

1815
// IssueSuggestions returns a list of issue suggestions
@@ -29,54 +26,11 @@ func IssueSuggestions(ctx *context.Context) {
2926
isPull = optional.Some(false)
3027
}
3128

32-
searchOpt := &issue_indexer.SearchOptions{
33-
Paginator: &db.ListOptions{
34-
Page: 0,
35-
PageSize: 5,
36-
},
37-
Keyword: keyword,
38-
RepoIDs: []int64{ctx.Repo.Repository.ID},
39-
IsPull: isPull,
40-
IsClosed: nil,
41-
SortBy: issue_indexer.SortByUpdatedDesc,
42-
}
43-
44-
ids, _, err := issue_indexer.SearchIssues(ctx, searchOpt)
45-
if err != nil {
46-
ctx.ServerError("SearchIssues", err)
47-
return
48-
}
49-
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
29+
suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull, keyword)
5030
if err != nil {
51-
ctx.ServerError("FindIssuesByIDs", err)
31+
ctx.ServerError("GetSuggestion", err)
5232
return
5333
}
5434

55-
suggestions := make([]*structs.Issue, 0, len(issues))
56-
57-
for _, issue := range issues {
58-
suggestion := &structs.Issue{
59-
ID: issue.ID,
60-
Index: issue.Index,
61-
Title: issue.Title,
62-
State: issue.State(),
63-
}
64-
65-
if issue.IsPull {
66-
if err := issue.LoadPullRequest(ctx); err != nil {
67-
ctx.ServerError("LoadPullRequest", err)
68-
return
69-
}
70-
if issue.PullRequest != nil {
71-
suggestion.PullRequest = &structs.PullRequestMeta{
72-
HasMerged: issue.PullRequest.HasMerged,
73-
IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
74-
}
75-
}
76-
}
77-
78-
suggestions = append(suggestions, suggestion)
79-
}
80-
8135
ctx.JSON(http.StatusOK, suggestions)
8236
}

Diff for: services/issue/suggestion.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package issue
5+
6+
import (
7+
"context"
8+
"strconv"
9+
10+
issues_model "code.gitea.io/gitea/models/issues"
11+
repo_model "code.gitea.io/gitea/models/repo"
12+
"code.gitea.io/gitea/modules/optional"
13+
"code.gitea.io/gitea/modules/structs"
14+
)
15+
16+
func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool], keyword string) ([]*structs.Issue, error) {
17+
var issues issues_model.IssueList
18+
var err error
19+
pageSize := 5
20+
if keyword == "" {
21+
issues, err = issues_model.FindLatestUpdatedIssues(ctx, repo.ID, isPull, pageSize)
22+
if err != nil {
23+
return nil, err
24+
}
25+
} else {
26+
indexKeyword, _ := strconv.ParseInt(keyword, 10, 64)
27+
var issueByIndex *issues_model.Issue
28+
var excludedID int64
29+
if indexKeyword > 0 {
30+
issueByIndex, err = issues_model.GetIssueByIndex(ctx, repo.ID, indexKeyword)
31+
if err != nil && !issues_model.IsErrIssueNotExist(err) {
32+
return nil, err
33+
}
34+
if issueByIndex != nil {
35+
excludedID = issueByIndex.ID
36+
pageSize--
37+
}
38+
}
39+
40+
issues, err = issues_model.FindIssuesSuggestionByKeyword(ctx, repo.ID, keyword, isPull, excludedID, pageSize)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
if issueByIndex != nil {
46+
issues = append([]*issues_model.Issue{issueByIndex}, issues...)
47+
}
48+
}
49+
50+
if err := issues.LoadPullRequests(ctx); err != nil {
51+
return nil, err
52+
}
53+
54+
suggestions := make([]*structs.Issue, 0, len(issues))
55+
for _, issue := range issues {
56+
suggestion := &structs.Issue{
57+
ID: issue.ID,
58+
Index: issue.Index,
59+
Title: issue.Title,
60+
State: issue.State(),
61+
}
62+
63+
if issue.IsPull && issue.PullRequest != nil {
64+
suggestion.PullRequest = &structs.PullRequestMeta{
65+
HasMerged: issue.PullRequest.HasMerged,
66+
IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
67+
}
68+
}
69+
suggestions = append(suggestions, suggestion)
70+
}
71+
72+
return suggestions, nil
73+
}

Diff for: services/issue/suggestion_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package issue
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/db"
10+
repo_model "code.gitea.io/gitea/models/repo"
11+
"code.gitea.io/gitea/models/unittest"
12+
"code.gitea.io/gitea/modules/optional"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func Test_Suggestion(t *testing.T) {
18+
assert.NoError(t, unittest.PrepareTestDatabase())
19+
20+
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
21+
22+
testCases := []struct {
23+
keyword string
24+
isPull optional.Option[bool]
25+
expectedIndexes []int64
26+
}{
27+
{
28+
keyword: "",
29+
expectedIndexes: []int64{5, 1, 4, 2, 3},
30+
},
31+
{
32+
keyword: "1",
33+
expectedIndexes: []int64{1},
34+
},
35+
{
36+
keyword: "issue",
37+
expectedIndexes: []int64{4, 1, 2, 3},
38+
},
39+
{
40+
keyword: "pull",
41+
expectedIndexes: []int64{5},
42+
},
43+
}
44+
45+
for _, testCase := range testCases {
46+
t.Run(testCase.keyword, func(t *testing.T) {
47+
issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull, testCase.keyword)
48+
assert.NoError(t, err)
49+
50+
issueIndexes := make([]int64, 0, len(issues))
51+
for _, issue := range issues {
52+
issueIndexes = append(issueIndexes, issue.Index)
53+
}
54+
assert.EqualValues(t, testCase.expectedIndexes, issueIndexes)
55+
})
56+
}
57+
}

0 commit comments

Comments
 (0)