Skip to content

Commit

Permalink
Move PartialMatch field to Explanation struct and update related logic (
Browse files Browse the repository at this point in the history
#2103)

- Previously, the `PartialMatch` field was returned for every hit, but
this caused confusion in complex queries involving disjunctions and
match queries. As a result, we moved `PartialMatch` to the score
explanation, where each subquery's explanation will include its own
`PartialMatch`. This field is set only if the query uses the
DisjunctionSearcher or scorer.
  • Loading branch information
CascadingRadium authored Feb 4, 2025
1 parent b7b67d3 commit 7c54de5
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 36 deletions.
7 changes: 4 additions & 3 deletions search/explanation.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ func init() {
}

type Explanation struct {
Value float64 `json:"value"`
Message string `json:"message"`
Children []*Explanation `json:"children,omitempty"`
Value float64 `json:"value"`
Message string `json:"message"`
PartialMatch bool `json:"partial_match,omitempty"`
Children []*Explanation `json:"children,omitempty"`
}

func (expl *Explanation) String() string {
Expand Down
2 changes: 1 addition & 1 deletion search/scorer/scorer_disjunction.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (s *DisjunctionQueryScorer) Score(ctx *search.SearchContext, constituents [
ce := make([]*search.Explanation, 2)
ce[0] = rawExpl
ce[1] = &search.Explanation{Value: coord, Message: fmt.Sprintf("coord(%d/%d)", countMatch, countTotal)}
newExpl = &search.Explanation{Value: newScore, Message: "product of:", Children: ce}
newExpl = &search.Explanation{Value: newScore, Message: "product of:", Children: ce, PartialMatch: countMatch != countTotal}
}

// reuse constituents[0] as the return value
Expand Down
7 changes: 0 additions & 7 deletions search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,6 @@ type DocumentMatch struct {
// results are completed
FieldTermLocations []FieldTermLocation `json:"-"`

// used to indicate if this match is a partial match
// in the case of a disjunction search
// this means that the match is partial because
// not all sub-queries matched
// if false, all the sub-queries matched
PartialMatch bool `json:"partial_match,omitempty"`

// 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
Expand Down
2 changes: 0 additions & 2 deletions search/searcher/search_disjunction_heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@ func (s *DisjunctionHeapSearcher) Next(ctx *search.SearchContext) (
rv = s.scorer.ScoreAndExplBreakdown(ctx, s.matching, s.matchingIdxs, nil, s.numSearchers)
} else {
// score this match
partialMatch := len(s.matching) != len(s.searchers)
rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers)
rv.PartialMatch = partialMatch
}
}

Expand Down
2 changes: 0 additions & 2 deletions search/searcher/search_disjunction_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,7 @@ func (s *DisjunctionSliceSearcher) Next(ctx *search.SearchContext) (
rv = s.scorer.ScoreAndExplBreakdown(ctx, s.matching, s.matchingIdxs, s.originalPos, s.numSearchers)
} else {
// score this match
partialMatch := len(s.matching) != len(s.searchers)
rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers)
rv.PartialMatch = partialMatch
}
}

Expand Down
73 changes: 52 additions & 21 deletions search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,7 @@ func TestMatchQueryPartialMatch(t *testing.T) {
mq1.SetField("description")

sr := NewSearchRequest(mq1)
sr.Explain = true
res, err := idx.Search(sr)
if err != nil {
t.Fatal(err)
Expand All @@ -1274,11 +1275,17 @@ func TestMatchQueryPartialMatch(t *testing.T) {
t.Errorf("Expected 2 results, but got: %v", res.Total)
}
for _, hit := range res.Hits {
if hit.ID == "doc1" && hit.PartialMatch {
t.Errorf("Expected doc1 to be a full match")
}
if hit.ID == "doc2" && !hit.PartialMatch {
t.Errorf("Expected doc2 to be a partial match")
switch hit.ID {
case "doc1":
if hit.Expl.PartialMatch {
t.Errorf("Expected doc1 to be a full match")
}
case "doc2":
if !hit.Expl.PartialMatch {
t.Errorf("Expected doc2 to be a partial match")
}
default:
t.Errorf("Unexpected document ID: %s", hit.ID)
}
}

Expand All @@ -1288,6 +1295,7 @@ func TestMatchQueryPartialMatch(t *testing.T) {
mq2.SetFuzziness(2)

sr = NewSearchRequest(mq2)
sr.Explain = true
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
Expand All @@ -1296,18 +1304,25 @@ func TestMatchQueryPartialMatch(t *testing.T) {
t.Errorf("Expected 2 results, but got: %v", res.Total)
}
for _, hit := range res.Hits {
if hit.ID == "doc1" && !hit.PartialMatch {
t.Errorf("Expected doc1 to be a partial match")
}
if hit.ID == "doc2" && hit.PartialMatch {
t.Errorf("Expected doc2 to be a full match")
switch hit.ID {
case "doc1":
if !hit.Expl.PartialMatch {
t.Errorf("Expected doc1 to be a partial match")
}
case "doc2":
if hit.Expl.PartialMatch {
t.Errorf("Expected doc2 to be a full match")
}
default:
t.Errorf("Unexpected document ID: %s", hit.ID)
}
}
// Test 3 - Two Docs hits, both full match
mq3 := NewMatchQuery("patrick")
mq3.SetField("description")

sr = NewSearchRequest(mq3)
sr.Explain = true
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
Expand All @@ -1316,18 +1331,25 @@ func TestMatchQueryPartialMatch(t *testing.T) {
t.Errorf("Expected 2 results, but got: %v", res.Total)
}
for _, hit := range res.Hits {
if hit.ID == "doc1" && hit.PartialMatch {
t.Errorf("Expected doc1 to be a full match")
}
if hit.ID == "doc2" && hit.PartialMatch {
t.Errorf("Expected doc2 to be a full match")
switch hit.ID {
case "doc1":
if hit.Expl.PartialMatch {
t.Errorf("Expected doc1 to be a full match")
}
case "doc2":
if hit.Expl.PartialMatch {
t.Errorf("Expected doc2 to be a full match")
}
default:
t.Errorf("Unexpected document ID: %s", hit.ID)
}
}
// Test 4 - Two Docs hits, both partial match
mq4 := NewMatchQuery("patrick stewart manager")
mq4.SetField("description")

sr = NewSearchRequest(mq4)
sr.Explain = true
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
Expand All @@ -1336,11 +1358,17 @@ func TestMatchQueryPartialMatch(t *testing.T) {
t.Errorf("Expected 2 results, but got: %v", res.Total)
}
for _, hit := range res.Hits {
if hit.ID == "doc1" && !hit.PartialMatch {
t.Errorf("Expected doc1 to be a partial match")
}
if hit.ID == "doc2" && !hit.PartialMatch {
t.Errorf("Expected doc2 to be a partial match")
switch hit.ID {
case "doc1":
if !hit.Expl.PartialMatch {
t.Errorf("Expected doc1 to be a full match")
}
case "doc2":
if !hit.Expl.PartialMatch {
t.Errorf("Expected doc2 to be a full match")
}
default:
t.Errorf("Unexpected document ID: %s", hit.ID)
}
}

Expand All @@ -1350,14 +1378,17 @@ func TestMatchQueryPartialMatch(t *testing.T) {
mq5.SetOperator(1)

sr = NewSearchRequest(mq5)
sr.Explain = true
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
}
if res.Total != 1 {
t.Errorf("Expected 1 result, but got: %v", res.Total)
}
if res.Hits[0].ID == "doc2" || res.Hits[0].PartialMatch {
hit := res.Hits[0]
fmt.Println(hit.Expl, hit.ID)
if hit.ID != "doc1" || hit.Expl.PartialMatch {
t.Errorf("Expected doc1 to be a full match")
}
}
Expand Down

0 comments on commit 7c54de5

Please sign in to comment.