Files
upstream-bleve/term_limit_integration_test.go
upstream-mirror f8d3d9f0cf
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
snapshot of https://github.com/blevesearch/bleve.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 22:27:16 +05:00

147 lines
5.3 KiB
Go

// 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 (
"context"
"fmt"
"strings"
"testing"
"github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/search/query"
)
// disjunctionOfTerms builds a disjunction of n distinct term queries over the
// "name" field. Each term query becomes one leaf term searcher at construction
// time, regardless of whether it matches any document.
func disjunctionOfTerms(prefix string, n int) query.Query {
dq := NewDisjunctionQuery()
for i := 0; i < n; i++ {
tq := NewTermQuery(fmt.Sprintf("%s%d", prefix, i))
tq.SetField("name")
dq.AddQuery(tq)
}
return dq
}
// conjunctionOfDisjunctions builds a conjunction of `groups` disjunctions, each
// containing `per` distinct term queries -> groups*per leaf term searchers, but
// no single fan-out node has more than `per` children.
func conjunctionOfDisjunctions(groups, per int) query.Query {
cq := NewConjunctionQuery()
for g := 0; g < groups; g++ {
cq.AddQuery(disjunctionOfTerms(fmt.Sprintf("g%d_", g), per))
}
return cq
}
// TestBleveMaxTermSearchers exercises the full bleveMaxTerms wiring end-to-end
// through SearchInContext: the per-search counter is installed from the ctx
// limit, every leaf term searcher is counted, and construction fails once the
// running total exceeds the limit. This complements TestRecordTermSearcher
// (which only covers the counter primitive) by validating that the counter is
// actually installed and consulted along the real search path.
func TestBleveMaxTermSearchers(t *testing.T) {
idx, err := NewMemOnly(NewIndexMapping())
if err != nil {
t.Fatal(err)
}
defer func() {
if cerr := idx.Close(); cerr != nil {
t.Fatal(cerr)
}
}()
// A couple of real documents so the term searchers open real readers.
if err := idx.Index("doc0", map[string]interface{}{"name": "alpha beta gamma"}); err != nil {
t.Fatal(err)
}
if err := idx.Index("doc1", map[string]interface{}{"name": "delta epsilon zeta"}); err != nil {
t.Fatal(err)
}
runSearch := func(q query.Query, limit int) error {
ctx := context.WithValue(context.Background(), search.MaxTermSearchersKey, limit)
_, serr := idx.SearchInContext(ctx, NewSearchRequest(q))
return serr
}
t.Run("disabled when limit unset (0)", func(t *testing.T) {
if err := runSearch(disjunctionOfTerms("t", 6), 0); err != nil {
t.Fatalf("limit 0 should disable the check, got: %v", err)
}
})
t.Run("disabled when limit is negative", func(t *testing.T) {
// A non-positive limit (as produced upstream when a request explicitly
// disables bleveMaxTerms) must be treated as "no limit", not enforced.
if err := runSearch(disjunctionOfTerms("t", 6), -1); err != nil {
t.Fatalf("negative limit should disable the check, got: %v", err)
}
})
t.Run("under the limit succeeds", func(t *testing.T) {
// 6 leaf term searchers, limit 100.
if err := runSearch(disjunctionOfTerms("t", 6), 100); err != nil {
t.Fatalf("6 term searchers under limit 100 should succeed, got: %v", err)
}
})
t.Run("exactly at the limit succeeds", func(t *testing.T) {
// 6 leaf term searchers, limit 6: 6 is not > 6, so it is allowed.
if err := runSearch(disjunctionOfTerms("t", 6), 6); err != nil {
t.Fatalf("6 term searchers at limit 6 should succeed, got: %v", err)
}
})
t.Run("over the limit fails with TooManyTermSearchers", func(t *testing.T) {
// 6 leaf term searchers, limit 5 -> trips at the 6th.
err := runSearch(disjunctionOfTerms("t", 6), 5)
if err == nil {
t.Fatal("6 term searchers over limit 5 should fail, got nil")
}
if !strings.Contains(err.Error(), "TooManyTermSearchers") {
t.Fatalf("expected a TooManyTermSearchers error, got: %v", err)
}
if !strings.Contains(err.Error(), "set to 5") {
t.Fatalf("error should report the effective limit (5), got: %v", err)
}
})
t.Run("counts globally across the whole query tree", func(t *testing.T) {
// 2 disjunctions * 4 terms = 8 leaf term searchers, but no single node
// has more than 4 children. With DisjunctionMaxClauseCount unset this
// can only be tripped by the global bleveMaxTerms count, proving the two
// checks are distinct: bleveMaxClauseCount is per-node, bleveMaxTerms is
// the sum across the tree.
err := runSearch(conjunctionOfDisjunctions(2, 4), 5)
if err == nil {
t.Fatal("8 term searchers over limit 5 should fail, got nil")
}
if !strings.Contains(err.Error(), "TooManyTermSearchers") {
t.Fatalf("expected a TooManyTermSearchers error, got: %v", err)
}
if strings.Contains(err.Error(), "TooManyClauses") {
t.Fatalf("should be the global term-count check, not the per-node clause check: %v", err)
}
// Same query, limit 8 -> allowed (8 is not > 8).
if err := runSearch(conjunctionOfDisjunctions(2, 4), 8); err != nil {
t.Fatalf("8 term searchers at limit 8 should succeed, got: %v", err)
}
})
}