snapshot of https://github.com/blevesearch/bleve.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
This commit is contained in:
184
earlystop_test.go
Normal file
184
earlystop_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2026 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bleve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/mapping"
|
||||
"github.com/blevesearch/bleve/v2/search/query"
|
||||
)
|
||||
|
||||
// buildEarlyStopIndex indexes n documents that all match "common", so a bounded
|
||||
// scan has plenty of matches left undrained.
|
||||
func buildEarlyStopIndex(t *testing.T, n int) Index {
|
||||
t.Helper()
|
||||
|
||||
dir, err := os.MkdirTemp("", "earlystop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(dir) })
|
||||
|
||||
im := mapping.NewIndexMapping()
|
||||
im.DefaultAnalyzer = "standard"
|
||||
idx, err := New(dir+"/i.bleve", im)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = idx.Close() })
|
||||
|
||||
batch := idx.NewBatch()
|
||||
for i := 0; i < n; i++ {
|
||||
if err := batch.Index(fmt.Sprintf("d%05d", i), map[string]interface{}{
|
||||
"body": fmt.Sprintf("common tag%d", i%10),
|
||||
"num": float64(i),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := idx.Batch(batch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func earlyStopQuery() query.Query {
|
||||
q := query.NewTermQuery("common")
|
||||
q.SetField("body")
|
||||
return q
|
||||
}
|
||||
|
||||
// TestEarlyStopBoundedScan is the contract for the bounded scan: with
|
||||
// Score="none" and a bounded Size, the request means "any Size+From matching
|
||||
// docs", so collection may stop once that many hits are in hand instead of
|
||||
// draining every match.
|
||||
//
|
||||
// Two things must hold. The caller must still get Size hits — stopping early must
|
||||
// not lose results it asked for. And Total must be reported as a lower bound
|
||||
// (TotalRelation "gte"), because the scan genuinely did not count the rest; a
|
||||
// caller that reads Total as exact would otherwise be silently misled.
|
||||
func TestEarlyStopBoundedScan(t *testing.T) {
|
||||
const n = 5000
|
||||
idx := buildEarlyStopIndex(t, n)
|
||||
|
||||
for _, size := range []int{1, 10, 100} {
|
||||
req := NewSearchRequest(earlyStopQuery())
|
||||
req.Size = size
|
||||
req.Score = ScoreNone
|
||||
res, err := idx.Search(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.Hits) != size {
|
||||
t.Errorf("size=%d: got %d hits, want %d — early stop dropped results the "+
|
||||
"caller asked for", size, len(res.Hits), size)
|
||||
}
|
||||
if res.TotalRelation != TotalRelationGte {
|
||||
t.Errorf("size=%d: TotalRelation=%q, want %q — Total is a lower bound once "+
|
||||
"the scan stops early, and saying otherwise misleads the caller",
|
||||
size, res.TotalRelation, TotalRelationGte)
|
||||
}
|
||||
if res.Total > uint64(n) {
|
||||
t.Errorf("size=%d: Total=%d exceeds the corpus size %d", size, res.Total, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEarlyStopDoesNotEngageWhenUnsafe pins the preconditions. Each of these
|
||||
// requests depends on documents the bounded scan would never look at, so it must
|
||||
// keep draining and report an exact Total. Getting any of these wrong is a silent
|
||||
// wrong-answer bug, not a slowdown:
|
||||
//
|
||||
// facets every match must be counted into the facet buckets
|
||||
// field sort the top-k by field value can lie anywhere in the match set
|
||||
// SearchAfter the cursor position depends on the full ordering
|
||||
// scoring Score != "none" means order depends on scores, not arrival
|
||||
func TestEarlyStopDoesNotEngageWhenUnsafe(t *testing.T) {
|
||||
const n = 2000
|
||||
idx := buildEarlyStopIndex(t, n)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
tweak func(*SearchRequest)
|
||||
}{
|
||||
{"facets", func(r *SearchRequest) {
|
||||
r.Score = ScoreNone
|
||||
r.AddFacet("tags", NewFacetRequest("body", 5))
|
||||
}},
|
||||
{"field-sort", func(r *SearchRequest) {
|
||||
r.Score = ScoreNone
|
||||
r.SortBy([]string{"num"})
|
||||
}},
|
||||
{"search-after", func(r *SearchRequest) {
|
||||
r.Score = ScoreNone
|
||||
r.SortBy([]string{"_id"})
|
||||
r.SearchAfter = []string{"d00010"}
|
||||
}},
|
||||
{"scoring-enabled", func(r *SearchRequest) {
|
||||
// Score defaults to full scoring; ordering depends on scores.
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := NewSearchRequest(earlyStopQuery())
|
||||
req.Size = 10
|
||||
tc.tweak(req)
|
||||
res, err := idx.Search(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", tc.name, err)
|
||||
}
|
||||
if res.TotalRelation != TotalRelationEq {
|
||||
t.Errorf("%s: TotalRelation=%q, want %q — the bounded scan engaged on a "+
|
||||
"request whose result depends on documents it would not visit",
|
||||
tc.name, res.TotalRelation, TotalRelationEq)
|
||||
}
|
||||
if res.Total != uint64(n) {
|
||||
t.Errorf("%s: Total=%d, want the exact %d", tc.name, res.Total, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEarlyStopHitsAreRealMatches guards the cheapest way for a bounded scan to be
|
||||
// wrong: returning documents that do not match. Order is not part of the contract
|
||||
// under Score="none", but membership is.
|
||||
func TestEarlyStopHitsAreRealMatches(t *testing.T) {
|
||||
idx := buildEarlyStopIndex(t, 1000)
|
||||
|
||||
q := query.NewTermQuery("tag3")
|
||||
q.SetField("body")
|
||||
req := NewSearchRequest(q)
|
||||
req.Size = 20
|
||||
req.Score = ScoreNone
|
||||
res, err := idx.Search(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.Hits) != 20 {
|
||||
t.Fatalf("got %d hits, want 20", len(res.Hits))
|
||||
}
|
||||
// tag3 was indexed on every doc where i%10 == 3.
|
||||
for _, h := range res.Hits {
|
||||
var i int
|
||||
if _, err := fmt.Sscanf(h.ID, "d%05d", &i); err != nil {
|
||||
t.Fatalf("unexpected id %q: %v", h.ID, err)
|
||||
}
|
||||
if i%10 != 3 {
|
||||
t.Errorf("id %s does not match tag3 — the bounded scan returned a non-match", h.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user