Repository: blevesearch/bleve
Pull request #1921 (2023-11-27 11:57:47)

change score breakdown from array to map
looking at https://issues.couchbase.com/browse/MB-59821
it seems that having score breakdown as a slice would be inoptimal in other use cases of disjunction searchers such as in Prefix and Wildcard queries. 
Here the number of searchers involved in a single disjunction searcher may be very large and can affect cpu execution time.
This approach changes the datatype of scorebreakdown to a map

CascadingRadium: yea but i wanted to check if the regressions go away with just the optimization in this PR. if the results arent so good, i will add that check, in a different PR.
abhinavdangeti: Per conversation w/ @CascadingRadium - new patch coming here addressing above comments.

Patch:
diff --git a/search/query/conjunction.go b/search/query/conjunction.go
index 0565e18f..ba600d64 100644
--- a/search/query/conjunction.go
+++ b/search/query/conjunction.go
@@ -26,9 +26,10 @@ import (
 )
 
 type ConjunctionQuery struct {
-	Conjuncts       []Query `json:"conjuncts"`
-	BoostVal        *Boost  `json:"boost,omitempty"`
-	queryStringMode bool
+	Conjuncts              []Query `json:"conjuncts"`
+	BoostVal               *Boost  `json:"boost,omitempty"`
+	retrieveScoreBreakdown bool
+	queryStringMode        bool
 }
 
 // NewConjunctionQuery creates a new compound Query.
@@ -48,6 +49,10 @@ func (q *ConjunctionQuery) Boost() float64 {
 	return q.BoostVal.Value()
 }
 
+func (q *ConjunctionQuery) RetrieveScoreBreakdown(b bool) {
+	q.retrieveScoreBreakdown = b
+}
+
 func (q *ConjunctionQuery) AddQuery(aq ...Query) {
 	for _, aaq := range aq {
 		q.Conjuncts = append(q.Conjuncts, aaq)
@@ -76,8 +81,9 @@ func (q *ConjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m
 	if len(ss) < 1 {
 		return searcher.NewMatchNoneSearcher(i)
 	}
+	nctx := context.WithValue(ctx, search.IncludeScoreBreakdownKey, q.retrieveScoreBreakdown)
 
-	return searcher.NewConjunctionSearcher(ctx, i, ss, options)
+	return searcher.NewConjunctionSearcher(nctx, i, ss, options)
 }
 
 func (q *ConjunctionQuery) Validate() error {
diff --git a/search/query/disjunction.go b/search/query/disjunction.go
index e008a042..b307865f 100644
--- a/search/query/disjunction.go
+++ b/search/query/disjunction.go
@@ -27,10 +27,15 @@ import (
 )
 
 type DisjunctionQuery struct {
-	Disjuncts       []Query `json:"disjuncts"`
-	BoostVal        *Boost  `json:"boost,omitempty"`
-	Min             float64 `json:"min"`
-	queryStringMode bool
+	Disjuncts              []Query `json:"disjuncts"`
+	BoostVal               *Boost  `json:"boost,omitempty"`
+	Min                    float64 `json:"min"`
+	retrieveScoreBreakdown bool
+	queryStringMode        bool
+}
+
+func (q *DisjunctionQuery) RetrieveScoreBreakdown(b bool) {
+	q.retrieveScoreBreakdown = b
 }
 
 // NewDisjunctionQuery creates a new compound Query.
@@ -86,7 +91,9 @@ func (q *DisjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m
 		return searcher.NewMatchNoneSearcher(i)
 	}
 
-	return searcher.NewDisjunctionSearcher(ctx, i, ss, q.Min, options)
+	nctx := context.WithValue(ctx, search.IncludeScoreBreakdownKey, q.retrieveScoreBreakdown)
+
+	return searcher.NewDisjunctionSearcher(nctx, i, ss, q.Min, options)
 }
 
 func (q *DisjunctionQuery) Validate() error {
diff --git a/search/scorer/scorer_conjunction.go b/search/scorer/scorer_conjunction.go
index c94aeebb..75d9df7b 100644
--- a/search/scorer/scorer_conjunction.go
+++ b/search/scorer/scorer_conjunction.go
@@ -48,21 +48,18 @@ func (s *ConjunctionQueryScorer) Score(ctx *search.SearchContext, constituents [
 	if s.options.Explain {
 		childrenExplanations = make([]*search.Explanation, len(constituents))
 	}
-	scoreBreakdown := make([]float64, len(constituents))
+	var scoreBreakdown map[int]float64
+	if originalPositions != nil {
+		scoreBreakdown = make(map[int]float64)
+	}
 	for i, docMatch := range constituents {
 		sum += docMatch.Score
-		if originalPositions != nil {
+		if scoreBreakdown != nil {
 			// for use in conjunction searcher
 			// the originalPositions are the positions of the searchers
 			// pre sort, since conjunction searcher sorts the searchers
 			// in order of their Count().
 			scoreBreakdown[originalPositions[i]] = docMatch.Score
-		} else {
-			// the indexes of searchers are the original searcher positions
-			// eg boolean searcher also uses the conjunction scorer,
-			// with index 0 being the must (conjunction) searcher
-			// and index 1 being the should (disjunction) searcher
-			scoreBreakdown[i] = docMatch.Score
 		}
 		if s.options.Explain {
 			childrenExplanations[i] = docMatch.Expl
diff --git a/search/scorer/scorer_disjunction.go b/search/scorer/scorer_disjunction.go
index db79025f..5bbd3f58 100644
--- a/search/scorer/scorer_disjunction.go
+++ b/search/scorer/scorer_disjunction.go
@@ -44,21 +44,31 @@ func NewDisjunctionQueryScorer(options search.SearcherOptions) *DisjunctionQuery
 }
 
 func (s *DisjunctionQueryScorer) Score(ctx *search.SearchContext, constituents []*search.DocumentMatch, countMatch, countTotal int,
-	matchingIdxs []int, originalPositions []int) *search.DocumentMatch {
+	matchingIdxs []int, originalPositions []int, includeScoreBreakdown bool) *search.DocumentMatch {
 
 	var sum float64
 	var childrenExplanations []*search.Explanation
 	if s.options.Explain {
 		childrenExplanations = make([]*search.Explanation, len(constituents))
 	}
-	scoreBreakdown := make([]float64, countTotal)
+	var scoreBreakdown map[int]float64
+	if includeScoreBreakdown {
+		scoreBreakdown = make(map[int]float64)
+	}
+
 	for i, docMatch := range constituents {
 		sum += docMatch.Score
-		if originalPositions != nil {
-			scoreBreakdown[originalPositions[matchingIdxs[i]]] = docMatch.Score
-		} else {
-			scoreBreakdown[matchingIdxs[i]] = docMatch.Score
+
+		if scoreBreakdown != nil {
+			if originalPositions != nil {
+				// scorer used in disjunction slice searcher
+				scoreBreakdown[originalPositions[matchingIdxs[i]]] = docMatch.Score
+			} else {
+				// scorer used in disjunction heap searcher
+				scoreBreakdown[matchingIdxs[i]] = docMatch.Score
+			}
 		}
+
 		if s.options.Explain {
 			childrenExplanations[i] = docMatch.Expl
 		}
diff --git a/search/search.go b/search/search.go
index 9b77251c..5e8bb7e7 100644
--- a/search/search.go
+++ b/search/search.go
@@ -177,11 +177,10 @@ type DocumentMatch struct {
 	// used to indicate the sub-scores that combined to form the
 	// final score for this document match.  This is only populated
 	// when the search request's query is a DisjunctionQuery
-	// or a ConjunctionQuery. The length of this slice will be
-	// the same as the number of sub-queries in the query.
-	// the order of the scores will match the order of the sub-queries
-	// in the query.
-	ScoreBreakdown []float64 `json:"score_breakdown,omitempty"`
+	// or a ConjunctionQuery. The map key is the index of the sub-query
+	// in the DisjunctionQuery or ConjunctionQuery. The map value is the
+	// sub-score for that sub-query.
+	ScoreBreakdown map[int]float64 `json:"score_breakdown,omitempty"`
 }
 
 func (dm *DocumentMatch) AddFieldValue(name string, value interface{}) {
diff --git a/search/searcher/ordered_searchers_list.go b/search/searcher/ordered_searchers_list.go
index 4e940922..ac9da563 100644
--- a/search/searcher/ordered_searchers_list.go
+++ b/search/searcher/ordered_searchers_list.go
@@ -18,22 +18,38 @@ import (
 	"github.com/blevesearch/bleve/v2/search"
 )
 
-type OrderedSearcherList struct {
+type OrderedSearcherList []search.Searcher
+
+// sort.Interface
+
+func (otrl OrderedSearcherList) Len() int {
+	return len(otrl)
+}
+
+func (otrl OrderedSearcherList) Less(i, j int) bool {
+	return otrl[i].Count() < otrl[j].Count()
+}
+
+func (otrl OrderedSearcherList) Swap(i, j int) {
+	otrl[i], otrl[j] = otrl[j], otrl[i]
+}
+
+type OrderedPositionalSearcherList struct {
 	searchers []search.Searcher
 	index     []int
 }
 
 // sort.Interface
 
-func (otrl OrderedSearcherList) Len() int {
+func (otrl OrderedPositionalSearcherList) Len() int {
 	return len(otrl.searchers)
 }
 
-func (otrl OrderedSearcherList) Less(i, j int) bool {
+func (otrl OrderedPositionalSearcherList) Less(i, j int) bool {
 	return otrl.searchers[i].Count() < otrl.searchers[j].Count()
 }
 
-func (otrl OrderedSearcherList) Swap(i, j int) {
+func (otrl OrderedPositionalSearcherList) Swap(i, j int) {
 	otrl.searchers[i], otrl.searchers[j] = otrl.searchers[j], otrl.searchers[i]
 	otrl.index[i], otrl.index[j] = otrl.index[j], otrl.index[i]
 }
diff --git a/search/searcher/search_conjunction.go b/search/searcher/search_conjunction.go
index 89a441d1..f3f40e59 100644
--- a/search/searcher/search_conjunction.go
+++ b/search/searcher/search_conjunction.go
@@ -50,17 +50,29 @@ func NewConjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
 	qsearchers []search.Searcher, options search.SearcherOptions) (
 	search.Searcher, error) {
 	// build the sorted downstream searchers
-	sortedSearchers := &OrderedSearcherList{
-		searchers: make([]search.Searcher, len(qsearchers)),
-		index:     make([]int, len(qsearchers)),
-	}
-	for i, searcher := range qsearchers {
-		sortedSearchers.searchers[i] = searcher
-		sortedSearchers.index[i] = i
+
+	var searchers OrderedSearcherList
+	var originalPos []int
+	retrieveScoreBreakdown, ok := ctx.Value(search.IncludeScoreBreakdownKey).(bool)
+	if ok && retrieveScoreBreakdown {
+		sortedSearchers := &OrderedPositionalSearcherList{
+			searchers: make([]search.Searcher, len(qsearchers)),
+			index:     make([]int, len(qsearchers)),
+		}
+		for i, searcher := range qsearchers {
+			sortedSearchers.searchers[i] = searcher
+			sortedSearchers.index[i] = i
+		}
+		sort.Sort(sortedSearchers)
+		searchers = sortedSearchers.searchers
+		originalPos = sortedSearchers.index
+	} else {
+		searchers = make(OrderedSearcherList, len(qsearchers))
+		for i, searcher := range qsearchers {
+			searchers[i] = searcher
+		}
+		sort.Sort(searchers)
 	}
-	sort.Sort(sortedSearchers)
-	searchers := sortedSearchers.searchers
-	originalPos := sortedSearchers.index
 
 	// attempt the "unadorned" conjunction optimization only when we
 	// do not need extra information like freq-norm's or term vectors
diff --git a/search/searcher/search_disjunction_heap.go b/search/searcher/search_disjunction_heap.go
index 51e21080..e26c8864 100644
--- a/search/searcher/search_disjunction_heap.go
+++ b/search/searcher/search_disjunction_heap.go
@@ -46,14 +46,15 @@ type SearcherCurr struct {
 type DisjunctionHeapSearcher struct {
 	indexReader index.IndexReader
 
-	numSearchers    int
-	scorer          *scorer.DisjunctionQueryScorer
-	min             int
-	queryNorm       float64
-	queryNormForKNN float64
-	initialized     bool
-	searchers       []search.Searcher
-	heap            []*SearcherCurr
+	numSearchers           int
+	scorer                 *scorer.DisjunctionQueryScorer
+	min                    int
+	queryNorm              float64
+	queryNormForKNN        float64
+	retrieveScoreBreakdown bool
+	initialized            bool
+	searchers              []search.Searcher
+	heap                   []*SearcherCurr
 
 	matching      []*search.DocumentMatch
 	matchingIdxs  []int
@@ -69,18 +70,20 @@ func newDisjunctionHeapSearcher(ctx context.Context, indexReader index.IndexRead
 	if limit && tooManyClauses(len(searchers)) {
 		return nil, tooManyClausesErr("", len(searchers))
 	}
+	retrieveScoreBreakdown, _ := ctx.Value(search.IncludeScoreBreakdownKey).(bool)
 
 	// build our searcher
 	rv := DisjunctionHeapSearcher{
-		indexReader:   indexReader,
-		searchers:     searchers,
-		numSearchers:  len(searchers),
-		scorer:        scorer.NewDisjunctionQueryScorer(options),
-		min:           int(min),
-		matching:      make([]*search.DocumentMatch, len(searchers)),
-		matchingCurrs: make([]*SearcherCurr, len(searchers)),
-		matchingIdxs:  make([]int, len(searchers)),
-		heap:          make([]*SearcherCurr, 0, len(searchers)),
+		indexReader:            indexReader,
+		searchers:              searchers,
+		numSearchers:           len(searchers),
+		scorer:                 scorer.NewDisjunctionQueryScorer(options),
+		min:                    int(min),
+		matching:               make([]*search.DocumentMatch, len(searchers)),
+		matchingCurrs:          make([]*SearcherCurr, len(searchers)),
+		matchingIdxs:           make([]int, len(searchers)),
+		retrieveScoreBreakdown: retrieveScoreBreakdown,
+		heap:                   make([]*SearcherCurr, 0, len(searchers)),
 	}
 	rv.computeQueryNorm()
 	return &rv, nil
@@ -194,7 +197,7 @@ func (s *DisjunctionHeapSearcher) Next(ctx *search.SearchContext) (
 			found = true
 			partialMatch := len(s.matching) != len(s.searchers)
 			// score this match
-			rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers, s.matchingIdxs, nil)
+			rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers, s.matchingIdxs, nil, s.retrieveScoreBreakdown)
 			rv.PartialMatch = partialMatch
 		}
 
diff --git a/search/searcher/search_disjunction_slice.go b/search/searcher/search_disjunction_slice.go
index e1459840..1af1e952 100644
--- a/search/searcher/search_disjunction_slice.go
+++ b/search/searcher/search_disjunction_slice.go
@@ -33,19 +33,20 @@ func init() {
 }
 
 type DisjunctionSliceSearcher struct {
-	indexReader     index.IndexReader
-	searchers       []search.Searcher
-	originalPos     []int
-	numSearchers    int
-	queryNorm       float64
-	queryNormForKNN float64
-	currs           []*search.DocumentMatch
-	scorer          *scorer.DisjunctionQueryScorer
-	min             int
-	matching        []*search.DocumentMatch
-	matchingIdxs    []int
-	initialized     bool
-	bytesRead       uint64
+	indexReader            index.IndexReader
+	searchers              []search.Searcher
+	originalPos            []int
+	numSearchers           int
+	queryNorm              float64
+	queryNormForKNN        float64
+	retrieveScoreBreakdown bool
+	currs                  []*search.DocumentMatch
+	scorer                 *scorer.DisjunctionQueryScorer
+	min                    int
+	matching               []*search.DocumentMatch
+	matchingIdxs           []int
+	initialized            bool
+	bytesRead              uint64
 }
 
 func newDisjunctionSliceSearcher(ctx context.Context, indexReader index.IndexReader,
@@ -55,29 +56,40 @@ func newDisjunctionSliceSearcher(ctx context.Context, indexReader index.IndexRea
 	if limit && tooManyClauses(len(qsearchers)) {
 		return nil, tooManyClausesErr("", len(qsearchers))
 	}
-	// build the downstream searchers
-	sortedSearchers := &OrderedSearcherList{
-		searchers: make([]search.Searcher, len(qsearchers)),
-		index:     make([]int, len(qsearchers)),
-	}
-	for i, searcher := range qsearchers {
-		sortedSearchers.searchers[i] = searcher
-		sortedSearchers.index[i] = i
+
+	var searchers OrderedSearcherList
+	var originalPos []int
+	retrieveScoreBreakdown, ok := ctx.Value(search.IncludeScoreBreakdownKey).(bool)
+	if ok && retrieveScoreBreakdown {
+		sortedSearchers := &OrderedPositionalSearcherList{
+			searchers: make([]search.Searcher, len(qsearchers)),
+			index:     make([]int, len(qsearchers)),
+		}
+		for i, searcher := range qsearchers {
+			sortedSearchers.searchers[i] = searcher
+			sortedSearchers.index[i] = i
+		}
+		sort.Sort(sortedSearchers)
+		searchers = sortedSearchers.searchers
+		originalPos = sortedSearchers.index
+	} else {
+		searchers = make(OrderedSearcherList, len(qsearchers))
+		for i, searcher := range qsearchers {
+			searchers[i] = searcher
+		}
+		sort.Sort(searchers)
 	}
-	// sort the searchers
-	sort.Sort(sort.Reverse(sortedSearchers))
-	// build our searcher
-	searchers := sortedSearchers.searchers
-	originalPos := sortedSearchers.index
 
 	rv := DisjunctionSliceSearcher{
-		indexReader:  indexReader,
-		searchers:    searchers,
-		originalPos:  originalPos,
-		numSearchers: len(searchers),
-		currs:        make([]*search.DocumentMatch, len(searchers)),
-		scorer:       scorer.NewDisjunctionQueryScorer(options),
-		min:          int(min),
+		indexReader:            indexReader,
+		searchers:              searchers,
+		originalPos:            originalPos,
+		numSearchers:           len(searchers),
+		currs:                  make([]*search.DocumentMatch, len(searchers)),
+		scorer:                 scorer.NewDisjunctionQueryScorer(options),
+		min:                    int(min),
+		retrieveScoreBreakdown: retrieveScoreBreakdown,
+
 		matching:     make([]*search.DocumentMatch, len(searchers)),
 		matchingIdxs: make([]int, len(searchers)),
 	}
@@ -195,7 +207,7 @@ func (s *DisjunctionSliceSearcher) Next(ctx *search.SearchContext) (
 			found = true
 			partialMatch := len(s.matching) != len(s.searchers)
 			// score this match
-			rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers, s.matchingIdxs, s.originalPos)
+			rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers, s.matchingIdxs, s.originalPos, s.retrieveScoreBreakdown)
 			rv.PartialMatch = partialMatch
 		}
 
diff --git a/search/util.go b/search/util.go
index b2cb62a2..e5712420 100644
--- a/search/util.go
+++ b/search/util.go
@@ -106,6 +106,7 @@ const (
 const SearchIncrementalCostKey = "_search_incremental_cost_key"
 const QueryTypeKey = "_query_type_key"
 const FuzzyMatchPhraseKey = "_fuzzy_match_phrase_key"
+const IncludeScoreBreakdownKey = "_include_score_breakdown_key"
 
 func RecordSearchCost(ctx context.Context,
 	msg SearchIncrementalCostCallbackMsg, bytes uint64) {
diff --git a/search_knn.go b/search_knn.go
index 380fec9d..ee2d3f64 100644
--- a/search_knn.go
+++ b/search_knn.go
@@ -180,9 +180,13 @@ func queryWithKNN(req *SearchRequest) (query.Query, error) {
 			}
 		}
 		if req.KNNOperator == knnOperatorAnd {
-			return query.NewConjunctionQuery(subQueries), nil
+			rv := query.NewConjunctionQuery(subQueries)
+			rv.RetrieveScoreBreakdown(true)
+			return rv, nil
 		} else if req.KNNOperator == knnOperatorOr || req.KNNOperator == "" {
-			return query.NewDisjunctionQuery(subQueries), nil
+			rv := query.NewDisjunctionQuery(subQueries)
+			rv.RetrieveScoreBreakdown(true)
+			return rv, nil
 		} else {
 			return nil, fmt.Errorf("unknown knn operator: %s", req.KNNOperator)
 		}
@@ -225,88 +229,84 @@ func adjustRequestSizeForKNN(req *SearchRequest, numIndexPartitions int) int {
 
 // heap impl
 type scoreHeap struct {
-	scoreBreakdown []*[]float64
-	sortIndex      int
+	scoreMaps []map[int]float64
+	sortIndex int
 }
 
-func (s *scoreHeap) Len() int { return len(s.scoreBreakdown) }
+func (s *scoreHeap) Len() int { return len(s.scoreMaps) }
 
 func (s *scoreHeap) Less(i, j int) bool {
-	return (*s.scoreBreakdown[i])[s.sortIndex] > (*s.scoreBreakdown[j])[s.sortIndex]
+	return (s.scoreMaps[i])[s.sortIndex] > (s.scoreMaps[j])[s.sortIndex]
 }
 
 func (s *scoreHeap) Swap(i, j int) {
-	s.scoreBreakdown[i], s.scoreBreakdown[j] = s.scoreBreakdown[j], s.scoreBreakdown[i]
+	s.scoreMaps[i], s.scoreMaps[j] = s.scoreMaps[j], s.scoreMaps[i]
 }
 
 func (s *scoreHeap) Push(x interface{}) {
-	s.scoreBreakdown = append(s.scoreBreakdown, x.(*[]float64))
+	s.scoreMaps = append(s.scoreMaps, x.(map[int]float64))
 }
 
 func (s *scoreHeap) Pop() interface{} {
-	old := s.scoreBreakdown
+	old := s.scoreMaps
 	n := len(old)
 	x := old[n-1]
-	s.scoreBreakdown = old[0 : n-1]
+	s.scoreMaps = old[0 : n-1]
 	return x
 }
 
 func mergeKNN(req *SearchRequest, sr *SearchResult) {
-	// index 0 of score breakdown is always tf-idf score
-	numKnnQuery := len(req.KNN)
 	maxHeap := &scoreHeap{
-		scoreBreakdown: make([]*[]float64, 0),
+		scoreMaps: make([]map[int]float64, 0),
 	}
-	for i := 0; i < numKnnQuery; i++ {
+	for i := 0; i < len(req.KNN); i++ {
 		kVal := req.KNN[i].K
 		maxHeap.sortIndex = i + 1
 		for _, hit := range sr.Hits {
-			heap.Push(maxHeap, &hit.ScoreBreakdown)
+			heap.Push(maxHeap, hit.ScoreBreakdown)
 		}
 		for maxHeap.Len() > 0 {
-			arr := heap.Pop(maxHeap).(*[]float64)
+			scBreakdown := heap.Pop(maxHeap).(map[int]float64)
 			if kVal > 0 {
 				kVal--
 			} else {
-				(*arr)[maxHeap.sortIndex] = 0
+				delete(scBreakdown, maxHeap.sortIndex)
 			}
 		}
 	}
-	nonZeroScoreHits := make([]*search.DocumentMatch, 0, len(sr.Hits))
 	maxScore := 0.0
 	var numHitsDropped uint64
+	numTotalSearchers := float64(len(req.KNN) + 1)
+	hitIdx := 0
 	for _, hit := range sr.Hits {
-		newScore := recomputeTotalScore(req.KNNOperator, hit)
+		numMatchedSearchers := float64(len(hit.ScoreBreakdown))
+		newScore := recomputeTotalScore(req.KNNOperator, hit, numMatchedSearchers, numTotalSearchers)
 		if newScore > 0 {
 			hit.Score = newScore
 			if newScore > maxScore {
 				maxScore = newScore
 			}
-			nonZeroScoreHits = append(nonZeroScoreHits, hit)
+			sr.Hits[hitIdx] = hit
+			hitIdx++
 		} else {
 			numHitsDropped++
 		}
 	}
-	sr.Hits = nonZeroScoreHits
+	sr.Hits = sr.Hits[:hitIdx]
 	sr.MaxScore = maxScore
 	sr.Total -= numHitsDropped
 }
 
-func recomputeTotalScore(operator knnOperator, hit *search.DocumentMatch) float64 {
-	totalScore := 0.0
-	numNonZero := 0.0
-	numTotal := float64(len(hit.ScoreBreakdown))
+func recomputeTotalScore(operator knnOperator, hit *search.DocumentMatch, numMatchedSearchers, numTotalSearchers float64) float64 {
+	if operator == knnOperatorAnd && numMatchedSearchers != numTotalSearchers {
+		return 0
+	}
+	var totalScore float64
 	for _, score := range hit.ScoreBreakdown {
-		if score != 0 {
-			numNonZero += 1
-		}
 		totalScore += score
 	}
 	if operator == knnOperatorOr || operator == "" {
-		coord := numNonZero / numTotal
-		totalScore = totalScore * coord
-	} else if operator == knnOperatorAnd && numNonZero != numTotal {
-		totalScore = 0
+		totalScore *= (numMatchedSearchers / numTotalSearchers)
 	}
 	return totalScore
 }


Test patch:
diff --git a/search_knn_test.go b/search_knn_test.go
index 589564dc..98b9cb49 100644
--- a/search_knn_test.go
+++ b/search_knn_test.go
@@ -222,7 +222,7 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 
 	type testResult struct {
 		score          float64
-		scoreBreakdown []float64
+		scoreBreakdown map[int]float64
 	}
 
 	type testCase struct {
@@ -240,15 +240,15 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          0.5547758085810349,
-					scoreBreakdown: []float64{0, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          0.3817633037007331,
-					scoreBreakdown: []float64{0, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
 				},
 				"doc28": {
 					score:          0.33983667469689355,
-					scoreBreakdown: []float64{0, 0.6796733493937871},
+					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
 				},
 			},
 		},
@@ -259,15 +259,15 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          0.5547758085810349,
-					scoreBreakdown: []float64{0, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          0.3817633037007331,
-					scoreBreakdown: []float64{0, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
 				},
 				"doc28": {
 					score:          0.33983667469689355,
-					scoreBreakdown: []float64{0, 0.6796733493937871},
+					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
 				},
 			},
 		},
@@ -277,16 +277,16 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			numIndexPartitions: 10,
 			expectedResults: map[string]testResult{
 				"doc29": {
-					score:          0.554775,
-					scoreBreakdown: []float64{0, 1.109551},
+					score:          0.5547758085810349,
+					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
 				},
 				"doc23": {
-					score:          0.381763,
-					scoreBreakdown: []float64{0, 0.763526},
+					score:          0.3817633037007331,
+					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
 				},
 				"doc28": {
-					score:          0.339836,
-					scoreBreakdown: []float64{0, 0.679673},
+					score:          0.33983667469689355,
+					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
 				},
 			},
 		},
@@ -297,27 +297,27 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          1.8859816084399936,
-					scoreBreakdown: []float64{0.7764299912779237, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237, 1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          1.8615644255330264,
-					scoreBreakdown: []float64{1.0980378181315602, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{0: 1.0980378181315602, 1: 0.7635266074014662},
 				},
 				"doc27": {
 					score:          0.4640056648691007,
-					scoreBreakdown: []float64{0.9280113297382014, 0},
+					scoreBreakdown: map[int]float64{0: 0.9280113297382014},
 				},
 				"doc28": {
 					score:          0.434037555556026,
-					scoreBreakdown: []float64{0.868075111112052, 0},
+					scoreBreakdown: map[int]float64{0: 0.868075111112052},
 				},
 				"doc30": {
 					score:          0.38821499563896184,
-					scoreBreakdown: []float64{0.7764299912779237, 0},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
 				},
 				"doc24": {
 					score:          0.38821499563896184,
-					scoreBreakdown: []float64{0.7764299912779237, 0},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
 				},
 			},
 		},
@@ -328,27 +328,27 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc23": {
 					score:          1.5207250366637521,
-					scoreBreakdown: []float64{0.7571984292622859, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{0: 0.7571984292622859, 1: 0.7635266074014662},
 				},
 				"doc29": {
 					score:          1.4834345192674083,
-					scoreBreakdown: []float64{0.3738829021053385, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{0: 0.3738829021053385, 1: 1.1095516171620698},
 				},
 				"doc24": {
 					score:          0.2677100734235977,
-					scoreBreakdown: []float64{0.5354201468471954, 0},
+					scoreBreakdown: map[int]float64{0: 0.5354201468471954},
 				},
 				"doc27": {
 					score:          0.22343776840593196,
-					scoreBreakdown: []float64{0.4468755368118639, 0},
+					scoreBreakdown: map[int]float64{0: 0.4468755368118639},
 				},
 				"doc28": {
 					score:          0.20900689401100958,
-					scoreBreakdown: []float64{0.41801378802201916, 0},
+					scoreBreakdown: map[int]float64{0: 0.41801378802201916},
 				},
 				"doc30": {
 					score:          0.18694145105266924,
-					scoreBreakdown: []float64{0.3738829021053385, 0},
+					scoreBreakdown: map[int]float64{0: 0.3738829021053385},
 				},
 			},
 		},
@@ -359,43 +359,43 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc7": {
 					score:          math.MaxFloat64 / 3.0,
-					scoreBreakdown: []float64{0, 0, math.MaxFloat64},
+					scoreBreakdown: map[int]float64{2: math.MaxFloat64},
 				},
 				"doc29": {
 					score:          0.6774608026082964,
-					scoreBreakdown: []float64{0.23161973134064517, 0.7845714725717996, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517, 1: 0.7845714725717996},
 				},
 				"doc23": {
 					score:          0.5783030702431613,
-					scoreBreakdown: []float64{0.32755976365480655, 0.5398948417099355, 0},
+					scoreBreakdown: map[int]float64{0: 0.32755976365480655, 1: 0.5398948417099355},
 				},
 				"doc3": {
 					score:          0.2550334160459894,
-					scoreBreakdown: []float64{0.7651002481379682, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.7651002481379682},
 				},
 				"doc13": {
 					score:          0.2208654210738964,
-					scoreBreakdown: []float64{0.6625962632216892, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.6625962632216892},
 				},
 				"doc5": {
 					score:          0.21180931116413285,
-					scoreBreakdown: []float64{0, 0, 0.6354279334923986},
+					scoreBreakdown: map[int]float64{2: 0.6354279334923986},
 				},
 				"doc27": {
 					score:          0.09227950890170131,
-					scoreBreakdown: []float64{0.27683852670510395, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.2768385267051039},
 				},
 				"doc28": {
 					score:          0.0863195764709126,
-					scoreBreakdown: []float64{0.2589587294127378, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.2589587294127378},
 				},
 				"doc30": {
 					score:          0.07720657711354839,
-					scoreBreakdown: []float64{0.23161973134064517, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
 				},
 				"doc24": {
 					score:          0.07720657711354839,
-					scoreBreakdown: []float64{0.23161973134064517, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
 				},
 			},
 		},
@@ -406,43 +406,43 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc7": {
 					score:          math.MaxFloat64 / 3.0,
-					scoreBreakdown: []float64{0, 0, math.MaxFloat64},
+					scoreBreakdown: map[int]float64{2: math.MaxFloat64},
 				},
 				"doc29": {
 					score:          0.567426591648309,
-					scoreBreakdown: []float64{0.06656841490066398, 0.7845714725717996, 0},
+					scoreBreakdown: map[int]float64{0: 0.06656841490066398, 1: 0.7845714725717996},
 				},
 				"doc23": {
 					score:          0.5639255136185979,
-					scoreBreakdown: []float64{0.3059934287179615, 0.5398948417099355, 0},
+					scoreBreakdown: map[int]float64{0: 0.3059934287179615, 1: 0.5398948417099355},
 				},
 				"doc5": {
 					score:          0.21180931116413285,
-					scoreBreakdown: []float64{0, 0, 0.6354279334923986},
+					scoreBreakdown: map[int]float64{2: 0.6354279334923986},
 				},
 				"doc3": {
 					score:          0.14064944169372873,
-					scoreBreakdown: []float64{0.42194832508118624, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.4219483250811862},
 				},
 				"doc13": {
 					score:          0.12180599172106943,
-					scoreBreakdown: []float64{0.3654179751632083, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.3654179751632083},
 				},
 				"doc27": {
 					score:          0.026521491065731144,
-					scoreBreakdown: []float64{0.07956447319719344, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.07956447319719343},
 				},
 				"doc28": {
 					score:          0.024808583220893122,
-					scoreBreakdown: []float64{0.07442574966267937, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.07442574966267937},
 				},
 				"doc30": {
 					score:          0.02218947163355466,
-					scoreBreakdown: []float64{0.06656841490066398, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.06656841490066398},
 				},
 				"doc24": {
 					score:          0.02218947163355466,
-					scoreBreakdown: []float64{0.06656841490066398, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.06656841490066398},
 				},
 			},
 		},
@@ -456,35 +456,35 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc24": {
 					score:          1.22027994094805,
-					scoreBreakdown: []float64{0.027736154383370196, 0.3471022633855392, 0.5085619451465123, 0.33687957803262836},
+					scoreBreakdown: map[int]float64{0: 0.027736154383370196, 1: 0.3471022633855392, 2: 0.5085619451465123, 3: 0.33687957803262836},
 				},
 				"doc17": {
 					score:          0.7851856993753307,
-					scoreBreakdown: []float64{0.3367753689069724, 0, 0.3892791754255179, 0.320859721501284},
+					scoreBreakdown: map[int]float64{0: 0.3367753689069724, 2: 0.3892791754255179, 3: 0.320859721501284},
 				},
 				"doc21": {
 					score:          0.5927148028393034,
-					scoreBreakdown: []float64{0.06974846263723515, 0, 0.3914133076090359, 0.3291246335394669},
+					scoreBreakdown: map[int]float64{0: 0.06974846263723515, 2: 0.3914133076090359, 3: 0.3291246335394669},
 				},
 				"doc14": {
 					score:          0.45680756875853035,
-					scoreBreakdown: []float64{0.5968461853543279, 0, 0, 0.31676895216273276},
+					scoreBreakdown: map[int]float64{0: 0.5968461853543279, 3: 0.31676895216273276},
 				},
 				"doc25": {
 					score:          0.292014972318407,
-					scoreBreakdown: []float64{0.17861510907524708, 0, 0.405414835561567, 0},
+					scoreBreakdown: map[int]float64{0: 0.17861510907524708, 2: 0.405414835561567},
 				},
 				"doc23": {
 					score:          0.24706850662359503,
-					scoreBreakdown: []float64{0.09761951136424651, 0, 0.39651750188294355, 0},
+					scoreBreakdown: map[int]float64{0: 0.09761951136424651, 2: 0.39651750188294355},
 				},
 				"doc15": {
 					score:          0.24489276164017085,
-					scoreBreakdown: []float64{0.17216818679645968, 0, 0, 0.317617336483882},
+					scoreBreakdown: map[int]float64{0: 0.17216818679645968, 3: 0.317617336483882},
 				},
 				"doc5": {
 					score:          0.10331722282971788,
-					scoreBreakdown: []float64{0, 0.4132688913188715, 0, 0},
+					scoreBreakdown: map[int]float64{1: 0.4132688913188715},
 				},
 			},
 		},
@@ -498,15 +498,15 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc24": {
 					score:          1.22027994094805,
-					scoreBreakdown: []float64{0.027736154383370196, 0.3471022633855392, 0.5085619451465123, 0.33687957803262836},
+					scoreBreakdown: map[int]float64{0: 0.027736154383370196, 1: 0.3471022633855392, 2: 0.5085619451465123, 3: 0.33687957803262836},
 				},
 				"doc17": {
 					score:          0.7851856993753307,
-					scoreBreakdown: []float64{0.3367753689069724, 0, 0.3892791754255179, 0.320859721501284},
+					scoreBreakdown: map[int]float64{0: 0.3367753689069724, 2: 0.3892791754255179, 3: 0.320859721501284},
 				},
 				"doc21": {
 					score:          0.5927148028393034,
-					scoreBreakdown: []float64{0.06974846263723515, 0, 0.3914133076090359, 0.3291246335394669},
+					scoreBreakdown: map[int]float64{0: 0.06974846263723515, 2: 0.3914133076090359, 3: 0.3291246335394669},
 				},
 			},
 		},
@@ -519,15 +519,15 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			expectedResults: map[string]testResult{
 				"doc14": {
 					score:          0.45680756875853035,
-					scoreBreakdown: []float64{0.5968461853543279, 0, 0, 0.31676895216273276},
+					scoreBreakdown: map[int]float64{0: 0.5968461853543279, 3: 0.31676895216273276},
 				},
 				"doc25": {
 					score:          0.292014972318407,
-					scoreBreakdown: []float64{0.17861510907524708, 0, 0.405414835561567, 0},
+					scoreBreakdown: map[int]float64{0: 0.17861510907524708, 2: 0.405414835561567},
 				},
 				"doc23": {
 					score:          0.24706850662359503,
-					scoreBreakdown: []float64{0.09761951136424651, 0, 0.39651750188294355, 0},
+					scoreBreakdown: map[int]float64{0: 0.09761951136424651, 2: 0.39651750188294355},
 				},
 			},
 		},
@@ -560,9 +560,6 @@ func runKNNTest(t *testing.T, randomizeDocuments bool) {
 			if !randomizeDocuments && expectScore != actualScore {
 				t.Fatalf("testcase %d failed: expected hit %d to have score %f, got %f", testCaseNum, i, expectedHit.score, hit.Score)
 			}
-			if len(hit.ScoreBreakdown) != len(expectedHit.scoreBreakdown) {
-				t.Fatalf("testcase %d failed: expected hit %d to have %d score breakdowns, got %d", testCaseNum, i, len(expectedHit.scoreBreakdown), len(hit.ScoreBreakdown))
-			}
 			if !randomizeDocuments {
 				actualScore := truncateScore(hit.ScoreBreakdown[0])
 				expectScore := truncateScore(expectedHit.scoreBreakdown[0])
@@ -603,7 +600,7 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 
 	type testResult struct {
 		score          float64
-		scoreBreakdown []float64
+		scoreBreakdown map[int]float64
 	}
 
 	testCases := []struct {
@@ -617,15 +614,15 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          0.5547758085810349,
-					scoreBreakdown: []float64{0, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          0.3817633037007331,
-					scoreBreakdown: []float64{0, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
 				},
 				"doc28": {
 					score:          0.33983667469689355,
-					scoreBreakdown: []float64{0, 0.6796733493937871},
+					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
 				},
 			},
 		},
@@ -635,15 +632,15 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          0.5547758085810349,
-					scoreBreakdown: []float64{0, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          0.3817633037007331,
-					scoreBreakdown: []float64{0, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{1: 0.7635266074014662},
 				},
 				"doc28": {
 					score:          0.33983667469689355,
-					scoreBreakdown: []float64{0, 0.6796733493937871},
+					scoreBreakdown: map[int]float64{1: 0.6796733493937871},
 				},
 			},
 		},
@@ -653,27 +650,27 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          1.8859816084399936,
-					scoreBreakdown: []float64{0.7764299912779237, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237, 1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          1.8615644255330264,
-					scoreBreakdown: []float64{1.0980378181315602, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{0: 1.0980378181315602, 1: 0.7635266074014662},
 				},
 				"doc27": {
 					score:          0.4640056648691007,
-					scoreBreakdown: []float64{0.9280113297382014, 0},
+					scoreBreakdown: map[int]float64{0: 0.9280113297382014},
 				},
 				"doc28": {
 					score:          0.434037555556026,
-					scoreBreakdown: []float64{0.868075111112052, 0},
+					scoreBreakdown: map[int]float64{0: 0.868075111112052},
 				},
 				"doc30": {
 					score:          0.38821499563896184,
-					scoreBreakdown: []float64{0.7764299912779237, 0},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
 				},
 				"doc24": {
 					score:          0.38821499563896184,
-					scoreBreakdown: []float64{0.7764299912779237, 0},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
 				},
 			},
 		},
@@ -683,27 +680,27 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 			expectedResults: map[string]testResult{
 				"doc29": {
 					score:          1.8859816084399936,
-					scoreBreakdown: []float64{0.7764299912779237, 1.1095516171620698},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237, 1: 1.1095516171620698},
 				},
 				"doc23": {
 					score:          1.8615644255330264,
-					scoreBreakdown: []float64{1.0980378181315602, 0.7635266074014662},
+					scoreBreakdown: map[int]float64{0: 1.0980378181315602, 1: 0.7635266074014662},
 				},
 				"doc27": {
 					score:          0.4640056648691007,
-					scoreBreakdown: []float64{0.9280113297382014, 0},
+					scoreBreakdown: map[int]float64{0: 0.9280113297382014},
 				},
 				"doc28": {
 					score:          0.434037555556026,
-					scoreBreakdown: []float64{0.868075111112052, 0},
+					scoreBreakdown: map[int]float64{0: 0.868075111112052},
 				},
 				"doc30": {
 					score:          0.38821499563896184,
-					scoreBreakdown: []float64{0.7764299912779237, 0},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
 				},
 				"doc24": {
 					score:          0.38821499563896184,
-					scoreBreakdown: []float64{0.7764299912779237, 0},
+					scoreBreakdown: map[int]float64{0: 0.7764299912779237},
 				},
 			},
 		},
@@ -713,43 +710,43 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 			expectedResults: map[string]testResult{
 				"doc7": {
 					score:          math.MaxFloat64 / 3.0,
-					scoreBreakdown: []float64{0, 0, math.MaxFloat64},
+					scoreBreakdown: map[int]float64{2: math.MaxFloat64},
 				},
 				"doc29": {
 					score:          0.6774608026082964,
-					scoreBreakdown: []float64{0.23161973134064517, 0.7845714725717996, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517, 1: 0.7845714725717996},
 				},
 				"doc23": {
 					score:          0.5783030702431613,
-					scoreBreakdown: []float64{0.32755976365480655, 0.5398948417099355, 0},
+					scoreBreakdown: map[int]float64{0: 0.32755976365480655, 1: 0.5398948417099355},
 				},
 				"doc3": {
 					score:          0.2550334160459894,
-					scoreBreakdown: []float64{0.7651002481379682, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.7651002481379682},
 				},
 				"doc13": {
 					score:          0.2208654210738964,
-					scoreBreakdown: []float64{0.6625962632216892, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.6625962632216892},
 				},
 				"doc5": {
 					score:          0.21180931116413285,
-					scoreBreakdown: []float64{0, 0, 0.6354279334923986},
+					scoreBreakdown: map[int]float64{2: 0.6354279334923986},
 				},
 				"doc27": {
 					score:          0.09227950890170131,
-					scoreBreakdown: []float64{0.27683852670510395, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.2768385267051039},
 				},
 				"doc28": {
 					score:          0.0863195764709126,
-					scoreBreakdown: []float64{0.2589587294127378, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.2589587294127378},
 				},
 				"doc30": {
 					score:          0.07720657711354839,
-					scoreBreakdown: []float64{0.23161973134064517, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
 				},
 				"doc24": {
 					score:          0.07720657711354839,
-					scoreBreakdown: []float64{0.23161973134064517, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
 				},
 			},
 		},
@@ -759,43 +756,43 @@ func TestSimilaritySearchMultipleSegments(t *testing.T) {
 			expectedResults: map[string]testResult{
 				"doc7": {
 					score:          math.MaxFloat64 / 3.0,
-					scoreBreakdown: []float64{0, 0, math.MaxFloat64},
+					scoreBreakdown: map[int]float64{2: math.MaxFloat64},
 				},
 				"doc29": {
 					score:          0.6774608026082964,
-					scoreBreakdown: []float64{0.23161973134064517, 0.7845714725717996, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517, 1: 0.7845714725717996},
 				},
 				"doc23": {
 					score:          0.5783030702431613,
-					scoreBreakdown: []float64{0.32755976365480655, 0.5398948417099355, 0},
+					scoreBreakdown: map[int]float64{0: 0.32755976365480655, 1: 0.5398948417099355},
 				},
 				"doc3": {
 					score:          0.2550334160459894,
-					scoreBreakdown: []float64{0.7651002481379682, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.7651002481379682},
 				},
 				"doc13": {
 					score:          0.2208654210738964,
-					scoreBreakdown: []float64{0.6625962632216892, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.6625962632216892},
 				},
 				"doc5": {
 					score:          0.21180931116413285,
-					scoreBreakdown: []float64{0, 0, 0.6354279334923986},
+					scoreBreakdown: map[int]float64{2: 0.6354279334923986},
 				},
 				"doc27": {
 					score:          0.09227950890170131,
-					scoreBreakdown: []float64{0.27683852670510395, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.2768385267051039},
 				},
 				"doc28": {
 					score:          0.0863195764709126,
-					scoreBreakdown: []float64{0.2589587294127378, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.2589587294127378},
 				},
 				"doc30": {
 					score:          0.07720657711354839,
-					scoreBreakdown: []float64{0.23161973134064517, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
 				},
 				"doc24": {
 					score:          0.07720657711354839,
-					scoreBreakdown: []float64{0.23161973134064517, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.23161973134064517},
 				},
 			},
 		},
diff --git a/search_test.go b/search_test.go
index fd56b095..815cb998 100644
--- a/search_test.go
+++ b/search_test.go
@@ -3434,91 +3434,104 @@ func TestScoreBreakdown(t *testing.T) {
 	type testResult struct {
 		docID          string // doc ID of the hit
 		score          float64
-		scoreBreakdown []float64
+		scoreBreakdown map[int]float64
 	}
 	type testStruct struct {
 		query      string
-		operator   int
+		typ        string
 		expectHits []testResult
 	}
 	testQueries := []testStruct{
 		{
-			// trigger conjunction searcher from match query with operator 1
+			// trigger conjunction searcher
 			// expect dolor and tempor to have higher term score - since present in lesser docs and having same term freq
-			query:    "lorem dolor amet adipiscing do tempor",
-			operator: 1,
+			query: `{"conjuncts":[{"term":"lorem","field":"text"},{"term":"dolor","field":"text"},{"term":"amet","field":"text"},{"term":"adipiscing","field":"text"},{"term":"do","field":"text"},{"term":"tempor","field":"text"}]}`,
+			typ:   "conjunction",
 			expectHits: []testResult{
 				{
 					docID:          "doc1",
 					score:          0.815545,
-					scoreBreakdown: []float64{0.11147035536863306, 0.18483179634014485, 0.11147035536863306, 0.11147035536863306, 0.11147035536863306, 0.18483179634014485},
+					scoreBreakdown: map[int]float64{0: 0.11147035536863306, 1: 0.18483179634014485, 2: 0.11147035536863306, 3: 0.11147035536863306, 4: 0.11147035536863306, 5: 0.18483179634014485},
 				},
 			},
 		},
 		{
-			// trigger disjunction heap searcher from match query with operator 0 (>10 searchers)
+			// trigger disjunction heap searcher (>10 searchers)
 			// expect score breakdown to have a 0 at BLANK
-			query:    "lorem BLANK ipsum BLANK BLANK dolor sit amet consectetur BLANK adipiscing BLANK elit sed do eiusmod tempor BLANK BLANK",
-			operator: 0,
+			query: `{"disjuncts":[{"term":"lorem","field":"text"},{"term":"blank","field":"text"},{"term":"ipsum","field":"text"},{"term":"blank","field":"text"},{"term":"blank","field":"text"},{"term":"dolor","field":"text"},{"term":"sit","field":"text"},{"term":"amet","field":"text"},{"term":"consectetur","field":"text"},{"term":"blank","field":"text"},{"term":"adipiscing","field":"text"},{"term":"blank","field":"text"},{"term":"elit","field":"text"},{"term":"sed","field":"text"},{"term":"do","field":"text"},{"term":"eiusmod","field":"text"},{"term":"tempor","field":"text"},{"term":"blank","field":"text"},{"term":"blank","field":"text"}]}`,
+			typ:   "disjunction",
 			expectHits: []testResult{
 				{
 					docID:          "doc1",
 					score:          0.3034548543819603,
-					scoreBreakdown: []float64{0.040398807605268316, 0, 0.040398807605268316, 0, 0, 0.0669862776967768, 0.040398807605268316, 0.040398807605268316, 0.0669862776967768, 0, 0.040398807605268316, 0, 0.040398807605268316, 0, 0.040398807605268316, 0.040398807605268316, 0.0669862776967768, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.040398807605268316, 2: 0.040398807605268316, 5: 0.0669862776967768, 6: 0.040398807605268316, 7: 0.040398807605268316, 8: 0.0669862776967768, 10: 0.040398807605268316, 12: 0.040398807605268316, 14: 0.040398807605268316, 15: 0.040398807605268316, 16: 0.0669862776967768},
 				},
 				{
 					docID:          "doc4",
 					score:          0.15956816751152955,
-					scoreBreakdown: []float64{0.04737179972998534, 0, 0.04737179972998534, 0, 0, 0, 0.04737179972998534, 0.04737179972998534, 0, 0, 0.04737179972998534, 0, 0.04737179972998534, 0, 0.04737179972998534, 0.04737179972998534, 0, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.04737179972998534, 2: 0.04737179972998534, 6: 0.04737179972998534, 7: 0.04737179972998534, 10: 0.04737179972998534, 12: 0.04737179972998534, 14: 0.04737179972998534, 15: 0.04737179972998534},
 				},
 				{
 					docID:          "doc2",
 					score:          0.14725661652397853,
-					scoreBreakdown: []float64{0.05470024557900147, 0, 0, 0, 0, 0.09069985124905133, 0, 0.05470024557900147, 0, 0, 0.05470024557900147, 0, 0, 0.15681178542754148, 0, 0.05470024557900147, 0, 0, 0},
+					scoreBreakdown: map[int]float64{0: 0.05470024557900147, 5: 0.09069985124905133, 7: 0.05470024557900147, 10: 0.05470024557900147, 13: 0.15681178542754148, 15: 0.05470024557900147},
 				},
 				{
 					docID:          "doc3",
 					score:          0.12637916362550797,
-					scoreBreakdown: []float64{0, 0, 0.05470024557900147, 0, 0, 0, 0.05470024557900147, 0, 0.09069985124905133, 0, 0, 0, 0.05470024557900147, 0, 0.05470024557900147, 0, 0.09069985124905133, 0, 0},
+					scoreBreakdown: map[int]float64{2: 0.05470024557900147, 6: 0.05470024557900147, 8: 0.09069985124905133, 12: 0.05470024557900147, 14: 0.05470024557900147, 16: 0.09069985124905133},
 				},
 			},
 		},
 		{
-			// trigger disjunction slice searcher from match query with operator 0 (< 10 searchers)
+			// trigger disjunction slice searcher (< 10 searchers)
 			// expect BLANK to give a 0 in score breakdown
-			query:    "BLANK lorem ipsum BLANK BLANK dolor sit BLANK",
-			operator: 0,
+			query: `{"disjuncts":[{"term":"blank","field":"text"},{"term":"lorem","field":"text"},{"term":"ipsum","field":"text"},{"term":"blank","field":"text"},{"term":"blank","field":"text"},{"term":"dolor","field":"text"},{"term":"sit","field":"text"},{"term":"blank","field":"text"}]}`,
+			typ:   "disjunction",
 			expectHits: []testResult{
 				{
 					docID:          "doc1",
 					score:          0.1340684440934241,
-					scoreBreakdown: []float64{0, 0.05756326446708409, 0.05756326446708409, 0, 0, 0.09544709478559595, 0.05756326446708409, 0},
+					scoreBreakdown: map[int]float64{1: 0.05756326446708409, 2: 0.05756326446708409, 5: 0.09544709478559595, 6: 0.05756326446708409},
 				},
 				{
 					docID:          "doc4",
 					score:          0.07593627256602972,
-					scoreBreakdown: []float64{0, 0.06749890894758198, 0.06749890894758198, 0, 0, 0, 0.06749890894758198, 0},
+					scoreBreakdown: map[int]float64{1: 0.06749890894758198, 2: 0.06749890894758198, 6: 0.06749890894758198},
 				},
 				{
 					docID:          "doc2",
 					score:          0.05179425287147191,
-					scoreBreakdown: []float64{0, 0.0779410306721006, 0, 0, 0, 0.129235980813787, 0, 0},
+					scoreBreakdown: map[int]float64{1: 0.0779410306721006, 5: 0.129235980813787},
 				},
 				{
 					docID:          "doc3",
 					score:          0.0389705153360503,
-					scoreBreakdown: []float64{0, 0, 0.0779410306721006, 0, 0, 0, 0.0779410306721006, 0},
+					scoreBreakdown: map[int]float64{2: 0.0779410306721006, 6: 0.0779410306721006},
 				},
 			},
 		},
 	}
-
 	for _, dtq := range testQueries {
-
-		mq := NewMatchQuery(dtq.query)
-		mq.SetField("text")
-		mq.SetOperator(query.MatchQueryOperator(dtq.operator))
-		sr := NewSearchRequest(mq)
+		var q query.Query
+		if dtq.typ == "conjunction" {
+			var rv query.ConjunctionQuery
+			err := json.Unmarshal([]byte(dtq.query), &rv)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rv.RetrieveScoreBreakdown(true)
+			q = &rv
+		} else if dtq.typ == "disjunction" {
+			var rv query.DisjunctionQuery
+			err := json.Unmarshal([]byte(dtq.query), &rv)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rv.RetrieveScoreBreakdown(true)
+			q = &rv
+		}
+		sr := NewSearchRequest(q)
 		sr.Explain = true
 		res, err := idx.Search(sr)
 		if err != nil {