fix(issue-search): single exclude query was erroneosly considered as must (#12589)

The bleve indexer included a fast path to consider a single token to be of MUST rather than should.
However, the condition missed an additional check and would erroneosly include a NOT as a MUST.

This was not spotted by the tests as such exclude queries were usually made along with another term to avoid noise.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12589
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Shiny Nematoda 2026-05-16 09:57:51 +02:00 committed by Gusted
parent cf3b4a160d
commit 17f5ce6ce3
2 changed files with 49 additions and 19 deletions

View file

@ -151,28 +151,33 @@ func (b *Indexer) Delete(_ context.Context, ids ...int64) error {
return batch.Flush()
}
// Search searches for issues by given conditions.
// Returns the matching issue IDs
func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
func termQuery(token internal.Token) query.Query {
innerQ := bleve.NewDisjunctionQuery(
inner_bleve.MatchPhraseQuery(token.Term, "title", issueIndexerAnalyzer, token.Fuzzy, 2.0),
inner_bleve.MatchPhraseQuery(token.Term, "content", issueIndexerAnalyzer, token.Fuzzy, 1.0),
inner_bleve.MatchPhraseQuery(token.Term, "comments", issueIndexerAnalyzer, token.Fuzzy, 1.0))
if issueID, err := token.ParseIssueReference(); err == nil {
idQuery := inner_bleve.NumericEqualityQuery(issueID, "index")
idQuery.SetBoost(20.0)
innerQ.AddQuery(idQuery)
}
return innerQ
}
// Create a boolean query with the provided tokens (if any)
func keywordQuery(tokens []internal.Token) *query.BooleanQuery {
q := bleve.NewBooleanQuery()
for _, token := range options.Tokens {
innerQ := bleve.NewDisjunctionQuery(
inner_bleve.MatchPhraseQuery(token.Term, "title", issueIndexerAnalyzer, token.Fuzzy, 2.0),
inner_bleve.MatchPhraseQuery(token.Term, "content", issueIndexerAnalyzer, token.Fuzzy, 1.0),
inner_bleve.MatchPhraseQuery(token.Term, "comments", issueIndexerAnalyzer, token.Fuzzy, 1.0))
if issueID, err := token.ParseIssueReference(); err == nil {
idQuery := inner_bleve.NumericEqualityQuery(issueID, "index")
idQuery.SetBoost(20.0)
innerQ.AddQuery(idQuery)
}
if len(options.Tokens) == 1 {
q.AddMust(innerQ)
break
}
// If there is only a single term the user is likely looking for
// a MUST rather than a SHOULD
if len(tokens) == 1 && tokens[0].Kind != internal.BoolOptNot {
q.AddMust(termQuery(tokens[0]))
return q
}
for _, token := range tokens {
innerQ := termQuery(token)
switch token.Kind {
case internal.BoolOptMust:
q.AddMust(innerQ)
@ -183,6 +188,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
}
}
return q
}
// Search searches for issues by given conditions.
// Returns the matching issue IDs
func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
q := keywordQuery(options.Tokens)
var filters []query.Query
if len(options.RepoIDs) > 0 || options.AllPublic {
var repoQueries []query.Query

View file

@ -171,6 +171,23 @@ var cases = []*testIndexerCase{
ExpectedIDs: []int64{1002},
ExpectedTotal: 1,
},
{
Name: "Keyword Exclude Only",
ExtraData: []*internal.IndexerData{
{ID: 1000, Title: "hello"},
{ID: 1001, Content: "hello world"},
{ID: 1002, Comments: []string{"hi", "hello world"}},
},
Keyword: "-hello",
SearchOptions: &internal.SearchOptions{
SortBy: internal.SortByCreatedDesc,
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
for _, hit := range result.Hits {
assert.NotContains(t, []int64{1000, 1001, 1002}, hit.ID)
}
},
},
{
Name: "Keyword Fuzzy",
ExtraData: []*internal.IndexerData{