Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2ed83c8
perf: §13 ternary heap (3-ary) for top-N collector to reduce siftDown…
steveyen Jun 9, 2026
ff904ef
perf: §1 MaxScore pruning with lazy segment-level maxTFNorm cache
steveyen Jun 9, 2026
932b0b9
perf: §8 MAXSCORE essential/non-essential partition for top-k pruning
steveyen Jun 9, 2026
57a45fb
perf: §15 per-segment score ceiling — skip entire segments below thre…
steveyen Jun 9, 2026
c3e8ce3
perf: §1 cache per-segment MaxTFNorm in TFR to halve initWANDMaxImpac…
steveyen Jun 11, 2026
ccfb6d3
perf: §15 minSegCeiling guard + DocumentMatch.Reset explicit zeroing
steveyen Jun 9, 2026
d58d962
perf: §9 lazy BM25 scoring in MAXSCORE hot path
steveyen Jun 9, 2026
e964a6d
perf: §22 replace IndexInternalID.Compare → binary.BigEndian.Uint64 i…
steveyen Jun 9, 2026
e10627c
perf: §22 fold wandAboveThreshold upper-bound sum into matching-colle…
steveyen Jun 9, 2026
6095e39
perf: §22 guard clear(scoreBreakdown) on nil; MergeFieldTermLocations…
steveyen Jun 9, 2026
84c431b
perf: §22 PutLazy — bypass full Reset for docs in the lazy BM25 MAXSC…
steveyen Jun 9, 2026
75d2c6f
perf: §15 segment boundary cache: avoid SegmentIndexOf per candidate
steveyen Jun 9, 2026
551412b
perf: §22 ScoreFast, concrete dispatch, PutUint64
steveyen Jun 9, 2026
af430c7
perf: §22 lazy check uses s.numSearchers; ScoreFast skips coord multiply
steveyen Jun 9, 2026
9b80f3b
perf: §22 cache lazyMode bool in hot cache line
steveyen Jun 9, 2026
5a37c35
perf: §22 skip empty bitmap allocs in disjunction:unadorned Finish()
steveyen Jun 11, 2026
61a9676
fix: §5 avoid TFR pool donation while caller still holds the pointer …
steveyen Jun 10, 2026
5851bdd
perf: §5 re-enable TFR recycling (DefaultFieldTFRCacheThreshold = 4)
steveyen Jun 10, 2026
e75a5ce
perf: §7 parallel segment search via ShardView TFRs
steveyen Jun 10, 2026
493e851
test: §7 integration tests for parallel segment search + nil-postings…
steveyen Jun 11, 2026
d981bab
perf: §7 per-request parallel segment search via context key
steveyen Jun 11, 2026
ff73efc
perf: §25 BM25 impact lookup table for fast per-posting scoring
steveyen Jun 10, 2026
e7cff72
perf: §25 use iterator-based NormColumnByte interface in bridge
steveyen Jun 10, 2026
6dfe132
build: §20 register zapx/v18 as default segment plugin
steveyen Jun 11, 2026
76ecebe
test: §4 update expected byte counts for zapx v18 format
steveyen Jun 10, 2026
d8e78be
feat: SearchResult.TotalRelation — signal when WAND pruning makes Tot…
steveyen Jun 17, 2026
23260ab
feat: SearchRequest.ScoreMode — opt-in competitive scoring (WAND prun…
steveyen Jun 18, 2026
3598891
feat: §34 parallel WAND with global MaxImpact ceilings
steveyen Jun 19, 2026
9e9f40b
feat: §33 concurrency gate + DF guard for §7 parallel segment search
steveyen Jun 19, 2026
e0f980d
feat: §35 dynamic shardK = max(count, floor) for §7 parallel search
steveyen Jun 19, 2026
e67b05a
test: update TestBytesRead faceted-query expected bytes from 105 to 137
steveyen Jun 20, 2026
e2151b6
fix: close orphaned sub-searchers after unadorned disjunction optimiz…
steveyen Jun 20, 2026
bd72f97
perf: specialize score-descending comparator + sort.Slice in heap Final
steveyen Jun 20, 2026
5c1c6c0
perf: cache normByteIterator type assertions in ISTFR to eliminate pe…
steveyen Jun 20, 2026
cd0e472
perf: eliminate per-candidate BigEndian decodes in nextMAXSCORE via c…
steveyen Jun 20, 2026
3905589
perf: fast-path prepareDocumentMatch + guard adjustDocumentMatch for …
steveyen Jun 20, 2026
c50f699
test: cover collectStoreList linked-list store (pre-perf-gar gap)
steveyen Jun 20, 2026
9f25e74
test: BM25 impact table correctness + DocumentMatch.Reset canary (in-…
steveyen Jun 20, 2026
a919c4d
test: sharedThreshold/dmMinHeap primitives + TotalRelation WAND cover…
steveyen Jun 20, 2026
f4c1555
fix: §34 parallel WAND must respect ScoreModeComplete (requestWAND flag)
steveyen Jun 21, 2026
2a622f5
fix: §33 DF guard under-estimates MSM candidate count — apply binomia…
steveyen Jun 21, 2026
66ebe41
fix: set WANDPruned correctly for MAXSCORE and §7 parallel shards
steveyen Jun 23, 2026
14f3a2e
collector: early-stop (bounded scan) for score=none + Size=k
steveyen Jun 24, 2026
8bd5f8d
fix go mods
capemox Jun 26, 2026
9f22bdd
redirect mods to blevesearch branches
capemox Jun 26, 2026
09a9a32
Adapt scorch SegmentIndexOf to bleve_index_api Value() single-return API
capemox Jul 7, 2026
fe3fe06
fix: build disjunction explanation before clearing rv.Expl
capemox Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions explain_disjunction_regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package bleve

import (
"fmt"
"math"
"os"
"strings"
"testing"

"github.com/blevesearch/bleve/v2/search"
)

func floatClose(a, b float64) bool {
return math.Abs(a-b) <= 1e-6*(1+math.Abs(b))
}

// walkExpl fails on any nil node and, for nodes whose message declares an
// aggregation ("sum of:" / "product of:"), verifies the node's Value equals the
// sum/product of its children. Nodes with a non-aggregating message (e.g.
// "saturation(...)") are only checked for nil children, not arithmetic.
// Returns the node count.
func walkExpl(t *testing.T, e *search.Explanation, path string) int {
t.Helper()
if e == nil {
t.Fatalf("NIL explanation node at %s", path)
}
n := 1
for i, c := range e.Children {
cp := fmt.Sprintf("%s > child[%d]", path, i)
if c == nil {
t.Fatalf("NIL child at %s (parent message=%q)", cp, e.Message)
}
n += walkExpl(t, c, cp)
}
if len(e.Children) == 0 {
return n
}
isSum := strings.Contains(e.Message, "sum of:")
isProduct := strings.Contains(e.Message, "product of:")
if !isSum && !isProduct {
return n // non-aggregating node (e.g. saturation): child is explanatory only
}
agg := 1.0
if isSum {
agg = 0.0
}
for _, c := range e.Children {
if isSum {
agg += c.Value
} else {
agg *= c.Value
}
}
kind := "product"
if isSum {
kind = "sum"
}
if !floatClose(agg, e.Value) {
t.Errorf("arithmetic mismatch at %s (message=%q): node.Value=%.8f but %s-of-children=%.8f",
path, e.Message, e.Value, kind, agg)
}
return n
}

// TestExplainDisjunctionTreeWellFormed is the regression test for the
// DisjunctionQueryScorer explain bug (first "sum of:" child came back nil).
// Runs under both scoring models; BM25 is the model FTS uses and the one the
// §25 impact-table / NormByte optimizations target.
func TestExplainDisjunctionTreeWellFormed(t *testing.T) {
for _, model := range []string{"", "bm25"} {
name := model
if name == "" {
name = "tfidf-default"
}
t.Run(name, func(t *testing.T) {
tmp, err := os.MkdirTemp("", "bleve-explain-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)

m := NewIndexMapping()
m.ScoringModel = model // "" also defaults to tf-idf; explicit for clarity

idx, err := New(tmp, m) // default index type = scorch (writes v18)
if err != nil {
t.Fatal(err)
}
defer idx.Close()

docs := map[string]string{
"a": "the quick brown fox jumps over the lazy dog",
"b": "quick brown quick brown clever",
"c": "brown bears and brown crates",
"d": "nothing relevant here at all",
}
batch := idx.NewBatch()
for id, text := range docs {
if err := batch.Index(id, map[string]any{"text": text}); err != nil {
t.Fatal(err)
}
}
if err := idx.Batch(batch); err != nil {
t.Fatal(err)
}

// Multi-term match (OR) => disjunction of TermSearchers => DisjunctionQueryScorer.Score.
q := NewMatchQuery("quick brown fox")
q.SetField("text")
req := NewSearchRequest(q)
req.Explain = true
req.Size = 10

res, err := idx.Search(req)
if err != nil {
t.Fatal(err)
}
if len(res.Hits) == 0 {
t.Fatal("expected hits")
}

for _, hit := range res.Hits {
if hit.Expl == nil {
t.Fatalf("doc %s: nil top-level explanation", hit.ID)
}
if !floatClose(hit.Score, hit.Expl.Value) {
t.Errorf("doc %s: hit.Score=%.8f != Expl.Value=%.8f", hit.ID, hit.Score, hit.Expl.Value)
}
nodes := walkExpl(t, hit.Expl, "doc "+hit.ID)
t.Logf("doc %s: score=%.6f, explanation nodes=%d\n%s", hit.ID, hit.Score, nodes, hit.Expl)
}
})
}
}
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ module github.com/blevesearch/bleve/v2
go 1.25.0

require (
github.com/RoaringBitmap/roaring/v2 v2.14.5
github.com/RoaringBitmap/roaring/v2 v2.18.2
github.com/bits-and-blooms/bitset v1.24.2
github.com/blevesearch/bleve_index_api v1.4.0
github.com/blevesearch/bleve_index_api v1.4.1-0.20260707065413-4b241f361ec2
github.com/blevesearch/geo v0.2.5
github.com/blevesearch/go-faiss v1.1.5
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475
Expand All @@ -25,7 +25,8 @@ require (
github.com/blevesearch/zapx/v14 v14.4.3
github.com/blevesearch/zapx/v15 v15.4.3
github.com/blevesearch/zapx/v16 v16.3.4
github.com/blevesearch/zapx/v17 v17.1.9
github.com/blevesearch/zapx/v17 v17.1.8
github.com/blevesearch/zapx/v18 v18.0.0-20260707065421-f1ceb9717cca
github.com/couchbase/moss v0.2.0
github.com/spf13/cobra v1.10.2
go.etcd.io/bbolt v1.4.0
Expand Down
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw=
github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/RoaringBitmap/roaring/v2 v2.18.2 h1:oPq3Cgx//iDuJQVp6xSInAKW34J9CEwE5GmLI2z+Eic=
github.com/RoaringBitmap/roaring/v2 v2.18.2/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0=
github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/blevesearch/bleve_index_api v1.4.0 h1:xoCC4dvTizjcsZu7yO9Ua+/259K09BrirjTwLkx2MpY=
github.com/blevesearch/bleve_index_api v1.4.0/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/bleve_index_api v1.4.1-0.20260707065413-4b241f361ec2 h1:Nc8lOn/fmchbYX2bi9hdAxhngW3XC0wf31xEjSKzY1U=
github.com/blevesearch/bleve_index_api v1.4.1-0.20260707065413-4b241f361ec2/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
github.com/blevesearch/go-faiss v1.1.5 h1:/IU5lkOahH9Ghfk9n3F6N0XD7PYVXZJWmNDc9TtXuco=
Expand Down Expand Up @@ -45,8 +45,10 @@ github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2l
github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
github.com/blevesearch/zapx/v17 v17.1.9 h1:K5MsArRyuwfylDTN1+cU7plGVIFz4gPoH4HD5M7I8ik=
github.com/blevesearch/zapx/v17 v17.1.9/go.mod h1:34TIaJmdo5hMh2IBLoE4Day65j7DJ++8s5trz1yrsGY=
github.com/blevesearch/zapx/v17 v17.1.8 h1:RhlEYVsjJuXbPaIHdKi3qbKpc8HLIToiTYf7A8CVkoo=
github.com/blevesearch/zapx/v17 v17.1.8/go.mod h1:34TIaJmdo5hMh2IBLoE4Day65j7DJ++8s5trz1yrsGY=
github.com/blevesearch/zapx/v18 v18.0.0-20260707065421-f1ceb9717cca h1:k6+l1lMGDpW+YHWEVVPLI8zozhIVzZ+jFlp4kYgmnRo=
github.com/blevesearch/zapx/v18 v18.0.0-20260707065421-f1ceb9717cca/go.mod h1:OQ/HrAtXAmUG40C02RCRb+5PjJIhcZFivmkw+acaGvw=
github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps=
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o=
Expand Down
13 changes: 12 additions & 1 deletion index/scorch/empty.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

package scorch

import segment "github.com/blevesearch/scorch_segment_api/v2"
import (
"github.com/RoaringBitmap/roaring/v2"
segment "github.com/blevesearch/scorch_segment_api/v2"
)

type emptyPostingsIterator struct{}

Expand All @@ -38,4 +41,12 @@ func (e *emptyPostingsIterator) ResetBytesRead(uint64) {}

func (e *emptyPostingsIterator) BytesWritten() uint64 { return 0 }

// Implement OptimizablePostingsIterator so that anEmptyPostingsIterator can
// participate in nested conjunction/disjunction optimizations without aborting
// them. ActualBitmap returning nil and DocNum1Hit returning false cause the
// iterator to contribute nothing to any AND or OR, which is correct.
func (e *emptyPostingsIterator) ActualBitmap() *roaring.Bitmap { return nil }
func (e *emptyPostingsIterator) DocNum1Hit() (uint64, bool) { return 0, false }
func (e *emptyPostingsIterator) ReplaceActual(*roaring.Bitmap) {}

var anEmptyPostingsIterator = &emptyPostingsIterator{}
34 changes: 13 additions & 21 deletions index/scorch/optimize.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,24 +308,6 @@ func (o *OptimizeTFRDisjunctionUnadorned) Finish() (rv index.Optimized, err erro
return nil, nil
}

for i := range o.snapshot.segment {
var cMax uint64

for _, tfr := range o.tfrs {
itr, ok := tfr.iterators[i].(segment.OptimizablePostingsIterator)
if !ok {
return nil, nil
}

if itr.ActualBitmap() != nil {
c := itr.ActualBitmap().GetCardinality()
if cMax < c {
cMax = c
}
}
}
}

// We use an artificial term and field because the optimized
// termFieldReader can represent multiple terms and fields.
oTFR := o.snapshot.unadornedTermFieldReader(
Expand Down Expand Up @@ -355,16 +337,26 @@ func (o *OptimizeTFRDisjunctionUnadorned) Finish() (rv index.Optimized, err erro
}
}

// Fast path: no hits in this segment — reuse the zero-alloc empty sentinel.
if len(actualBMs) == 0 && len(docNums) == 0 {
oTFR.iterators[i] = anEmptyPostingsIterator
continue
}

// Fast path: exactly one 1-hit doc with no bitmaps.
if len(actualBMs) == 0 && len(docNums) == 1 {
oTFR.iterators[i] = newUnadornedPostingsIteratorFrom1Hit(uint64(docNums[0]))
continue
}

var bm *roaring.Bitmap
if len(actualBMs) > 2 {
bm = roaring.HeapOr(actualBMs...)
} else if len(actualBMs) == 2 {
bm = roaring.Or(actualBMs[0], actualBMs[1])
} else if len(actualBMs) == 1 {
bm = actualBMs[0].Clone()
}

if bm == nil {
} else {
bm = roaring.New()
}

Expand Down
Loading