Repository: blevesearch/bleve

Issue:
bug: Flaky Filter clause for slices in Boolean Queries
## The Problem

When using the `Filter` clause with a Boolean query to filter results; filter by an integer among a slice of integers, it produces flaky results.

## Example

```diff
diff --git a/search_test.go b/search_test.go
index 4e98c6d..9479ef8 100644
--- a/search_test.go
+++ b/search_test.go
@@ -4558,11 +4558,16 @@ func TestFilteredBooleanQuery(t *testing.T) {
 	titleMapping := mapping.NewTextFieldMapping()
 	titleMapping.Analyzer = en.AnalyzerName
 
+	tagsMapping := mapping.NewNumericFieldMapping()
+	tagsMapping.Store = false
+	tagsMapping.IncludeInAll = false
+
 	priceMapping := mapping.NewNumericFieldMapping()
 	imap.DefaultMapping.AddFieldMappingsAt("genre", genreMapping)
 	imap.DefaultMapping.AddFieldMappingsAt("author", authorMapping)
 	imap.DefaultMapping.AddFieldMappingsAt("title", titleMapping)
 	imap.DefaultMapping.AddFieldMappingsAt("price", priceMapping)
+	imap.DefaultMapping.AddFieldMappingsAt("tags", tagsMapping)
 
 	idx, err := New(tmpIndexPath, imap)
 	if err != nil {
@@ -4586,24 +4591,28 @@ func TestFilteredBooleanQuery(t *testing.T) {
 			"author": "J.D. Salinger",
 			"genre":  "fiction",
 			"price":  9.99,
+			"tags":   []int{1, 2, 3},
 		},
 		{
 			"title":  "Sapiens",
 			"author": "Yuval Noah Harari",
 			"genre":  "non-fiction",
 			"price":  14.29,
+			"tags":   []int{2},
 		},
 		{
 			"title":  "To Kill a Mockingbird",
 			"author": "Harper Lee",
 			"genre":  "fiction",
 			"price":  12,
+			"tags":   []int{},
 		},
 		{
 			"title":  "The Power of Habit",
 			"author": "Charles Duhigg",
 			"genre":  "self-help",
 			"price":  26,
+			"tags":   []int{1, 2},
 		},
 		{
 			"title":  "The Great Gatsby",
@@ -4622,12 +4631,14 @@ func TestFilteredBooleanQuery(t *testing.T) {
 			"author": "Tara Westover",
 			"genre":  "non-fiction",
 			"price":  18,
+			"tags":   []int{1, 2},
 		},
 		{
 			"title":  "1984",
 			"author": "George Orwell",
 			"genre":  "fiction",
 			"price":  20,
+			"tags":   []int{3},
 		},
 	}
 
@@ -4771,6 +4782,28 @@ func TestFilteredBooleanQuery(t *testing.T) {
 			t.Fatalf("expected score %f, got %f", unfilteredScore, res.Hits[i].Score)
 		}
 	}
+
+	p := float64(3)
+	incl := true
+	eqlFilter := NewNumericRangeInclusiveQuery(&p, &p, &incl, &incl)
+	eqlFilter.SetField("tags")
+	q = NewBooleanQuery()
+	q.AddFilter(eqlFilter)
+
+	req = NewSearchRequest(q)
+	req.Fields = []string{"title"}
+	res, err = idx.Search(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if res.Hits.Len() != 2 {
+		t.Fatalf("expected two hits, found '%d'", res.Hits.Len())
+	}
+
+	if res.Total != 2 {
+		t.Fatalf("expected two total, found '%d'", res.Total)
+	}
 }
 
 func TestGeoDistanceInSortAlias(t *testing.T) {
```

## Expected

The test should pass consistently. Currently, it fails if you run the test a few times.

Discussion:
@abhinavdangeti: @CascadingRadium would you take a look into this?@snematoda: That was fast! Thank you!

Solution patch:
diff --git a/search/query/boolean.go b/search/query/boolean.go
index 41dbcb40..c5e92895 100644
--- a/search/query/boolean.go
+++ b/search/query/boolean.go
@@ -185,17 +185,36 @@ func (q *BooleanQuery) Searcher(ctx context.Context, i index.IndexReader, m mapp
 		if err != nil {
 			return nil, err
 		}
+		var init bool
+		var refDoc *search.DocumentMatch
 		filterFunc = func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
-			// Attempt to advance the filter searcher to the document identified by
-			// the base searcher's (unfiltered boolean) current result (d.IndexInternalID).
-			//
-			// If the filter searcher successfully finds a document with the same
-			// internal ID, it means the document satisfies the filter and should be kept.
-			//
-			// If the filter searcher returns an error, does not find a matching document,
-			// or finds a document with a different internal ID, the document should be discarded.
-			dm, err := filterSearcher.Advance(sctx, d.IndexInternalID)
-			return err == nil && dm != nil && bytes.Equal(dm.IndexInternalID, d.IndexInternalID)
+			// Initialize the reference document to point
+			// to the first document in the filterSearcher
+			var err error
+			if !init {
+				refDoc, err = filterSearcher.Next(sctx)
+				if err != nil {
+					return false
+				}
+				init = true
+			}
+			if refDoc == nil {
+				// filterSearcher is exhausted, d is not in filter
+				return false
+			}
+			// Compare document IDs
+			cmp := bytes.Compare(refDoc.IndexInternalID, d.IndexInternalID)
+			if cmp < 0 {
+				// filterSearcher is behind the current document, Advance() it
+				refDoc, err = filterSearcher.Advance(sctx, d.IndexInternalID)
+				if err != nil || refDoc == nil {
+					return false
+				}
+				// After advance, check if they're now equal
+				return bytes.Equal(refDoc.IndexInternalID, d.IndexInternalID)
+			}
+			// cmp >= 0: either equal (match) or filterSearcher is ahead (no match)
+			return cmp == 0
 		}
 	}
 



Test patch:
diff --git a/search_test.go b/search_test.go
index 4e98c6de..829696ae 100644
--- a/search_test.go
+++ b/search_test.go
@@ -4558,11 +4558,16 @@ func TestFilteredBooleanQuery(t *testing.T) {
 	titleMapping := mapping.NewTextFieldMapping()
 	titleMapping.Analyzer = en.AnalyzerName
 
+	tagsMapping := mapping.NewNumericFieldMapping()
+	tagsMapping.Store = false
+	tagsMapping.IncludeInAll = false
+
 	priceMapping := mapping.NewNumericFieldMapping()
 	imap.DefaultMapping.AddFieldMappingsAt("genre", genreMapping)
 	imap.DefaultMapping.AddFieldMappingsAt("author", authorMapping)
 	imap.DefaultMapping.AddFieldMappingsAt("title", titleMapping)
 	imap.DefaultMapping.AddFieldMappingsAt("price", priceMapping)
+	imap.DefaultMapping.AddFieldMappingsAt("tags", tagsMapping)
 
 	idx, err := New(tmpIndexPath, imap)
 	if err != nil {
@@ -4586,36 +4591,42 @@ func TestFilteredBooleanQuery(t *testing.T) {
 			"author": "J.D. Salinger",
 			"genre":  "fiction",
 			"price":  9.99,
+			"tags":   []int{1, 2, 3},
 		},
 		{
 			"title":  "Sapiens",
 			"author": "Yuval Noah Harari",
 			"genre":  "non-fiction",
 			"price":  14.29,
+			"tags":   []int{2},
 		},
 		{
 			"title":  "To Kill a Mockingbird",
 			"author": "Harper Lee",
 			"genre":  "fiction",
 			"price":  12,
+			"tags":   []int{},
 		},
 		{
 			"title":  "The Power of Habit",
 			"author": "Charles Duhigg",
 			"genre":  "self-help",
 			"price":  26,
+			"tags":   []int{1, 2},
 		},
 		{
 			"title":  "The Great Gatsby",
 			"author": "F. Scott Fitzgerald",
 			"genre":  "fiction",
 			"price":  22,
+			"tags":   []int{1, 2},
 		},
 		{
 			"title":  "Atomic Habits",
 			"author": "James Clear",
 			"genre":  "self-help",
 			"price":  15,
+			"tags":   []int{3},
 		},
 		{
 			"title":  "Educated",
@@ -4771,6 +4782,28 @@ func TestFilteredBooleanQuery(t *testing.T) {
 			t.Fatalf("expected score %f, got %f", unfilteredScore, res.Hits[i].Score)
 		}
 	}
+	// A filtered boolean query requesting all books with tag value 3
+	// The filter is in the Filter clause
+	// Two books have tag value 3
+	p := float64(3)
+	incl := true
+	eqlFilter := NewNumericRangeInclusiveQuery(&p, &p, &incl, &incl)
+	eqlFilter.SetField("tags")
+	q = NewBooleanQuery()
+	q.AddFilter(eqlFilter)
+
+	req = NewSearchRequest(q)
+	req.Fields = []string{"title"}
+	res, err = idx.Search(req)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.Hits.Len() != 2 {
+		t.Fatalf("expected two hits, found '%d'", res.Hits.Len())
+	}
+	if res.Total != 2 {
+		t.Fatalf("expected two total, found '%d'", res.Total)
+	}
 }
 
 func TestGeoDistanceInSortAlias(t *testing.T) {