Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MB-55479: Reflect partial or complete match for match queries #1848

Merged
merged 3 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ type DocumentMatch struct {
// be later incorporated into the Locations map when search
// 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:"-"`
CascadingRadium marked this conversation as resolved.
Show resolved Hide resolved
}

func (dm *DocumentMatch) AddFieldValue(name string, value interface{}) {
Expand Down
2 changes: 2 additions & 0 deletions search/searcher/search_disjunction_heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,10 @@ func (s *DisjunctionHeapSearcher) Next(ctx *search.SearchContext) (
for !found && len(s.matching) > 0 {
if len(s.matching) >= s.min {
found = true
partialMatch := len(s.matching) != len(s.searchers)
// score this match
rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers)
rv.PartialMatch = partialMatch
}

// invoke next on all the matching searchers
Expand Down
2 changes: 2 additions & 0 deletions search/searcher/search_disjunction_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,10 @@ func (s *DisjunctionSliceSearcher) Next(ctx *search.SearchContext) (
for !found && len(s.matching) > 0 {
if len(s.matching) >= s.min {
found = true
partialMatch := len(s.matching) != len(s.searchers)
// score this match
rv = s.scorer.Score(ctx, s.matching, len(s.matching), s.numSearchers)
rv.PartialMatch = partialMatch
}

// invoke next on all the matching searchers
Expand Down
130 changes: 130 additions & 0 deletions search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,136 @@ func TestDisjunctionQueryIncorrectMin(t *testing.T) {
}
}

func TestMatchQueryPartialMatch(t *testing.T) {
tmpIndexPath := createTmpIndexPath(t)
defer cleanupTmpIndexPath(t, tmpIndexPath)
idx, err := New(tmpIndexPath, NewIndexMapping())
if err != nil {
t.Fatal(err)
}
defer func() {
err = idx.Close()
if err != nil {
t.Fatal(err)
}
}()
doc1 := map[string]interface{}{
"description": "Patrick is first name Stewart is last name",
}
doc2 := map[string]interface{}{
"description": "Manager given name is Patrick",
}
batch := idx.NewBatch()
if err = batch.Index("doc1", doc1); err != nil {
t.Fatal(err)
}
if err = batch.Index("doc2", doc2); err != nil {
t.Fatal(err)
}
if err = idx.Batch(batch); err != nil {
t.Fatal(err)
}
// Test 1 - Both Docs hit, doc 1 = Full Match and doc 2 = Partial Match
mq1 := NewMatchQuery("patrick stewart")
mq1.SetField("description")

sr := NewSearchRequest(mq1)
res, err := idx.Search(sr)
if err != nil {
t.Fatal(err)
}
if res.Total != 2 {
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")
}
}

// Test 2 - Both Docs hit, doc 1 = Partial Match and doc 2 = Full Match
mq2 := NewMatchQuery("paltric manner")
mq2.SetField("description")
mq2.SetFuzziness(2)

sr = NewSearchRequest(mq2)
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
}
if res.Total != 2 {
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")
}
}
// Test 3 - Two Docs hits, both full match
mq3 := NewMatchQuery("patrick")
mq3.SetField("description")

sr = NewSearchRequest(mq3)
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
}
if res.Total != 2 {
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")
}
}
// Test 4 - Two Docs hits, both partial match
mq4 := NewMatchQuery("patrick stewart manager")
mq4.SetField("description")

sr = NewSearchRequest(mq4)
res, err = idx.Search(sr)
if err != nil {
t.Fatal(err)
}
if res.Total != 2 {
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")
}
}

// Test 5 - Match Query AND operator always results in full match
mq5 := NewMatchQuery("patrick stewart")
mq5.SetField("description")
mq5.SetOperator(1)

sr = NewSearchRequest(mq5)
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 {
t.Errorf("Expected doc1 to be a full match")
}
}

func TestBooleanShouldMinPropagation(t *testing.T) {
tmpIndexPath := createTmpIndexPath(t)
defer cleanupTmpIndexPath(t, tmpIndexPath)
Expand Down