feat: support filtering for issues with multiple assignees (#10552)

Stores the entire list of AssigneeIDs for each issue in the indexer.
This fixes the bug where there were missing entries for issues with assignees while filtering.

Note: Will re-index all issues

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10552
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Shiny Nematoda <snematoda.751k2@aleeas.com>
Co-committed-by: Shiny Nematoda <snematoda.751k2@aleeas.com>
This commit is contained in:
Shiny Nematoda 2026-02-07 22:37:33 +01:00 committed by Gusted
parent cc47a4057f
commit 239d7168e1
6 changed files with 33 additions and 16 deletions

View file

@ -23,7 +23,7 @@ import (
const (
issueIndexerAnalyzer = "issueIndexer"
issueIndexerDocType = "issueIndexerDocType"
issueIndexerLatestVersion = 6
issueIndexerLatestVersion = 7
)
const unicodeNormalizeName = "unicodeNormalize"
@ -82,7 +82,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) {
docMapping.AddFieldMappingsAt("project_id", numberFieldMapping)
docMapping.AddFieldMappingsAt("project_board_id", numberFieldMapping)
docMapping.AddFieldMappingsAt("poster_id", numberFieldMapping)
docMapping.AddFieldMappingsAt("assignee_id", numberFieldMapping)
docMapping.AddFieldMappingsAt("assignee_ids", numberFieldMapping)
docMapping.AddFieldMappingsAt("mention_ids", numberFieldMapping)
docMapping.AddFieldMappingsAt("reviewed_ids", numberFieldMapping)
docMapping.AddFieldMappingsAt("review_requested_ids", numberFieldMapping)
@ -245,7 +245,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
"project_id": options.ProjectID,
"project_board_id": options.ProjectColumnID,
"poster_id": options.PosterID,
"assignee_id": options.AssigneeID,
"assignee_ids": options.AssigneeID,
"mention_ids": options.MentionID,
"reviewed_ids": options.ReviewedID,
"review_requested_ids": options.ReviewRequestedID,

View file

@ -18,7 +18,7 @@ import (
)
const (
issueIndexerLatestVersion = 2
issueIndexerLatestVersion = 3
// multi-match-types, currently only 2 types are used
// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
esMultiMatchTypeBestFields = "best_fields"
@ -69,7 +69,7 @@ const (
"project_id": { "type": "long", "index": true },
"project_board_id": { "type": "long", "index": true },
"poster_id": { "type": "long", "index": true },
"assignee_id": { "type": "long", "index": true },
"assignee_ids": { "type": "long", "index": true },
"mention_ids": { "type": "long", "index": true },
"reviewed_ids": { "type": "long", "index": true },
"review_requested_ids": { "type": "long", "index": true },
@ -233,7 +233,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
}
if options.AssigneeID.Has() {
query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value()))
query.Must(elastic.NewTermQuery("assignee_ids", options.AssigneeID.Value()))
}
if options.MentionID.Has() {

View file

@ -30,7 +30,7 @@ type IndexerData struct {
ProjectID int64 `json:"project_id"`
ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible
PosterID int64 `json:"poster_id"`
AssigneeID int64 `json:"assignee_id"`
AssigneeIDs []int64 `json:"assignee_ids"`
MentionIDs []int64 `json:"mention_ids"`
ReviewedIDs []int64 `json:"reviewed_ids"`
ReviewRequestedIDs []int64 `json:"review_requested_ids"`

View file

@ -461,10 +461,10 @@ var cases = []*testIndexerCase{
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, 5)
for _, v := range result.Hits {
assert.Equal(t, int64(1), data[v.ID].AssigneeID)
assert.Contains(t, data[v.ID].AssigneeIDs, int64(1))
}
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.AssigneeID == 1
return slices.Contains(v.AssigneeIDs, 1)
}), result.Total)
},
},
@ -479,10 +479,10 @@ var cases = []*testIndexerCase{
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, 5)
for _, v := range result.Hits {
assert.Equal(t, int64(0), data[v.ID].AssigneeID)
assert.Equal(t, []int64{0}, data[v.ID].AssigneeIDs)
}
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.AssigneeID == 0
return slices.Contains(v.AssigneeIDs, 0)
}), result.Total)
},
},
@ -840,6 +840,14 @@ func generateDefaultIndexerData() []*internal.IndexerData {
subscriberIDs[i] = int64(i) + 1 // SubscriberID should not be 0
}
assigneeIDs := make([]int64, 0, 2)
{
if issueIndex%7 == 0 { // If divisible by 7 we insert 1 too to test multiple assignees
assigneeIDs = append(assigneeIDs, 1)
}
assigneeIDs = append(assigneeIDs, issueIndex%10)
}
data = append(data, &internal.IndexerData{
ID: id,
Index: issueIndex,
@ -856,7 +864,7 @@ func generateDefaultIndexerData() []*internal.IndexerData {
ProjectID: issueIndex % 5,
ProjectColumnID: issueIndex % 6,
PosterID: id%10 + 1, // PosterID should not be 0
AssigneeID: issueIndex % 10,
AssigneeIDs: assigneeIDs,
MentionIDs: mentionIDs,
ReviewedIDs: reviewedIDs,
ReviewRequestedIDs: reviewRequestedIDs,

View file

@ -18,7 +18,7 @@ import (
)
const (
issueIndexerLatestVersion = 3
issueIndexerLatestVersion = 4
// TODO: make this configurable if necessary
maxTotalHits = 10000
@ -67,7 +67,7 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer {
"project_id",
"project_board_id",
"poster_id",
"assignee_id",
"assignee_ids",
"mention_ids",
"reviewed_ids",
"review_requested_ids",
@ -183,7 +183,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
}
if options.AssigneeID.Has() {
query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value()))
query.And(inner_meilisearch.NewFilterEq("assignee_ids", options.AssigneeID.Value()))
}
if options.MentionID.Has() {

View file

@ -50,6 +50,15 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
labels = append(labels, label.ID)
}
assigneeIDs := make([]int64, 0, len(issue.Assignees))
if len(issue.Assignees) != 0 {
for _, assignee := range issue.Assignees {
assigneeIDs = append(assigneeIDs, assignee.ID)
}
} else {
assigneeIDs = append(assigneeIDs, 0)
}
mentionIDs, err := issues_model.GetIssueMentionIDs(ctx, issueID)
if err != nil {
return nil, false, err
@ -108,7 +117,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
ProjectID: projectID,
ProjectColumnID: issue.ProjectColumnID(ctx),
PosterID: issue.PosterID,
AssigneeID: issue.AssigneeID,
AssigneeIDs: assigneeIDs,
MentionIDs: mentionIDs,
ReviewedIDs: reviewedIDs,
ReviewRequestedIDs: reviewRequestedIDs,