Skip to content

Commit 26be7c8

Browse files
meili-bors[bot]migueltargacurquiza
authored
Merge #543
543: Add facet search method r=curquiza a=migueltarga # Pull Request ## Related issue Fixes #470 ## What does this PR do? - Implements Facet Search introduced on version 1.3 Reference: https://www.meilisearch.com/docs/reference/api/facet_search ## PR checklist Please check if your PR fulfills the following requirements: - [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)? - [x] Have you read the contributing guidelines? - [x] Have you made sure that the title is accurate and descriptive of the changes? Thank you so much for contributing to Meilisearch! Co-authored-by: Miguel Targa <[email protected]> Co-authored-by: Clémentine <[email protected]>
2 parents aea59d3 + c4e85d6 commit 26be7c8

File tree

5 files changed

+790
-206
lines changed

5 files changed

+790
-206
lines changed

.code-samples.meilisearch.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ multi_search_1: |-
9898
},
9999
},
100100
})
101+
facet_search_1: |-
102+
client.Index("books").FacetSearch(&meilisearch.FacetSearchRequest{
103+
FacetQuery: "fiction",
104+
FacetName: "genres",
105+
Filter: "rating > 3",
106+
})
107+
facet_search_3: |-
108+
client.Index("books").FacetSearch(&meilisearch.FacetSearchRequest{
109+
FacetQuery: "c",
110+
FacetName: "genres",
111+
})
101112
delete_tasks_1: |-
102113
client.DeleteTaks(&meilisearch.DeleteTasksQuery{
103114
UIDS: []int64{1, 2},
@@ -645,7 +656,11 @@ getting_started_configure_settings: |-
645656
getting_started_faceting: |-
646657
client.Index("movies").UpdateFaceting(&meilisearch.Faceting{
647658
MaxValuesPerFacet: 2,
659+
SortFacetValuesBy: {
660+
"*": "count"
661+
}
648662
})
663+
649664
getting_started_pagination: |-
650665
client.Index("movies").UpdatePagination(&meilisearch.Pagination{
651666
MaxTotalHits: 500,

index_facet_search.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package meilisearch
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
)
8+
9+
var ErrNoFacetSearchRequest = errors.New("no search facet request provided")
10+
11+
func (i Index) FacetSearch(request *FacetSearchRequest) (*json.RawMessage, error) {
12+
if request == nil {
13+
return nil, ErrNoFacetSearchRequest
14+
}
15+
16+
searchPostRequestParams := FacetSearchPostRequestParams(request)
17+
18+
resp := &json.RawMessage{}
19+
20+
req := internalRequest{
21+
endpoint: "/indexes/" + i.UID + "/facet-search",
22+
method: http.MethodPost,
23+
contentType: contentTypeJSON,
24+
withRequest: searchPostRequestParams,
25+
withResponse: resp,
26+
acceptedStatusCodes: []int{http.StatusOK},
27+
functionName: "FacetSearch",
28+
}
29+
30+
if err := i.client.executeRequest(req); err != nil {
31+
return nil, err
32+
}
33+
34+
return resp, nil
35+
}
36+
37+
func FacetSearchPostRequestParams(request *FacetSearchRequest) map[string]interface{} {
38+
params := make(map[string]interface{}, 22)
39+
40+
if request.Q != "" {
41+
params["q"] = request.Q
42+
}
43+
if request.FacetName != "" {
44+
params["facetName"] = request.FacetName
45+
}
46+
if request.FacetQuery != "" {
47+
params["facetQuery"] = request.FacetQuery
48+
}
49+
if request.Filter != "" {
50+
params["filter"] = request.Filter
51+
}
52+
if request.MatchingStrategy != "" {
53+
params["matchingStrategy"] = request.MatchingStrategy
54+
}
55+
if len(request.AttributesToSearchOn) != 0 {
56+
params["attributesToSearchOn"] = request.AttributesToSearchOn
57+
}
58+
59+
return params
60+
}

index_facet_search_test.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package meilisearch
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestIndex_FacetSearch(t *testing.T) {
11+
type args struct {
12+
UID string
13+
PrimaryKey string
14+
client *Client
15+
request *FacetSearchRequest
16+
filterableAttributes []string
17+
}
18+
19+
tests := []struct {
20+
name string
21+
args args
22+
want *FacetSearchResponse
23+
wantErr bool
24+
}{
25+
{
26+
name: "TestIndexBasicFacetSearch",
27+
args: args{
28+
UID: "indexUID",
29+
client: defaultClient,
30+
request: &FacetSearchRequest{
31+
FacetName: "tag",
32+
FacetQuery: "Novel",
33+
},
34+
filterableAttributes: []string{"tag"},
35+
},
36+
want: &FacetSearchResponse{
37+
FacetHits: []interface{}{
38+
map[string]interface{}{
39+
"value": "Novel", "count": float64(5),
40+
},
41+
},
42+
FacetQuery: "Novel",
43+
},
44+
wantErr: false,
45+
},
46+
{
47+
name: "TestIndexFacetSearchWithFilter",
48+
args: args{
49+
UID: "indexUID",
50+
client: defaultClient,
51+
request: &FacetSearchRequest{
52+
FacetName: "tag",
53+
FacetQuery: "Novel",
54+
Filter: "tag = 'Novel'",
55+
},
56+
filterableAttributes: []string{"tag"},
57+
},
58+
want: &FacetSearchResponse{
59+
FacetHits: []interface{}{
60+
map[string]interface{}{
61+
"value": "Novel", "count": float64(5),
62+
},
63+
},
64+
FacetQuery: "Novel",
65+
},
66+
wantErr: false,
67+
},
68+
{
69+
name: "TestIndexFacetSearchWithMatchingStrategy",
70+
args: args{
71+
UID: "indexUID",
72+
client: defaultClient,
73+
request: &FacetSearchRequest{
74+
FacetName: "tag",
75+
FacetQuery: "Novel",
76+
MatchingStrategy: "frequency",
77+
},
78+
filterableAttributes: []string{"tag"},
79+
},
80+
want: &FacetSearchResponse{
81+
FacetHits: []interface{}{
82+
map[string]interface{}{
83+
"value": "Novel", "count": float64(5),
84+
},
85+
},
86+
FacetQuery: "Novel",
87+
},
88+
wantErr: false,
89+
},
90+
{
91+
name: "TestIndexFacetSearchWithAttributesToSearchOn",
92+
args: args{
93+
UID: "indexUID",
94+
client: defaultClient,
95+
request: &FacetSearchRequest{
96+
FacetName: "tag",
97+
FacetQuery: "Novel",
98+
AttributesToSearchOn: []string{"tag"},
99+
},
100+
filterableAttributes: []string{"tag"},
101+
},
102+
want: &FacetSearchResponse{
103+
FacetHits: []interface{}{
104+
map[string]interface{}{
105+
"value": "Novel", "count": float64(5),
106+
},
107+
},
108+
FacetQuery: "Novel",
109+
},
110+
wantErr: false,
111+
},
112+
{
113+
name: "TestIndexFacetSearchWithNoFacetSearchRequest",
114+
args: args{
115+
UID: "indexUID",
116+
client: defaultClient,
117+
request: nil,
118+
},
119+
want: nil,
120+
wantErr: true,
121+
},
122+
{
123+
name: "TestIndexFacetSearchWithNoFacetName",
124+
args: args{
125+
UID: "indexUID",
126+
client: defaultClient,
127+
request: &FacetSearchRequest{
128+
FacetQuery: "Novel",
129+
},
130+
},
131+
want: nil,
132+
wantErr: true,
133+
},
134+
{
135+
name: "TestIndexFacetSearchWithNoFacetQuery",
136+
args: args{
137+
UID: "indexUID",
138+
client: defaultClient,
139+
request: &FacetSearchRequest{
140+
FacetName: "tag",
141+
},
142+
},
143+
want: nil,
144+
wantErr: true,
145+
},
146+
{
147+
name: "TestIndexFacetSearchWithNoFilterableAttributes",
148+
args: args{
149+
UID: "indexUID",
150+
client: defaultClient,
151+
request: &FacetSearchRequest{
152+
FacetName: "tag",
153+
FacetQuery: "Novel",
154+
},
155+
},
156+
want: nil,
157+
wantErr: true,
158+
},
159+
{
160+
name: "TestIndexFacetSearchWithQ",
161+
args: args{
162+
UID: "indexUID",
163+
client: defaultClient,
164+
request: &FacetSearchRequest{
165+
Q: "query",
166+
FacetName: "tag",
167+
},
168+
filterableAttributes: []string{"tag"},
169+
},
170+
want: &FacetSearchResponse{
171+
FacetHits: []interface{}{},
172+
FacetQuery: "",
173+
},
174+
wantErr: false,
175+
},
176+
}
177+
178+
for _, tt := range tests {
179+
t.Run(tt.name, func(t *testing.T) {
180+
SetUpIndexForFaceting()
181+
c := tt.args.client
182+
i := c.Index(tt.args.UID)
183+
t.Cleanup(cleanup(c))
184+
185+
if len(tt.args.filterableAttributes) > 0 {
186+
updateFilter, err := i.UpdateFilterableAttributes(&tt.args.filterableAttributes)
187+
require.NoError(t, err)
188+
testWaitForTask(t, i, updateFilter)
189+
}
190+
191+
gotRaw, err := i.FacetSearch(tt.args.request)
192+
193+
if tt.wantErr {
194+
require.Error(t, err)
195+
require.Nil(t, gotRaw)
196+
return
197+
}
198+
199+
require.NoError(t, err)
200+
// Unmarshall the raw response from FacetSearch into a FacetSearchResponse
201+
var got FacetSearchResponse
202+
err = json.Unmarshal(*gotRaw, &got)
203+
require.NoError(t, err, "error unmarshalling raw got FacetSearchResponse")
204+
205+
require.Equal(t, len(tt.want.FacetHits), len(got.FacetHits))
206+
for len := range got.FacetHits {
207+
require.Equal(t, tt.want.FacetHits[len].(map[string]interface{})["value"], got.FacetHits[len].(map[string]interface{})["value"])
208+
require.Equal(t, tt.want.FacetHits[len].(map[string]interface{})["count"], got.FacetHits[len].(map[string]interface{})["count"])
209+
}
210+
require.Equal(t, tt.want.FacetQuery, got.FacetQuery)
211+
})
212+
}
213+
}

types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,21 @@ type MultiSearchResponse struct {
394394
Results []SearchResponse `json:"results"`
395395
}
396396

397+
type FacetSearchRequest struct {
398+
FacetName string `json:"facetName,omitempty"`
399+
FacetQuery string `json:"facetQuery,omitempty"`
400+
Q string `json:"q,omitempty"`
401+
Filter string `json:"filter,omitempty"`
402+
MatchingStrategy string `json:"matchingStrategy,omitempty"`
403+
AttributesToSearchOn []string `json:"attributesToSearchOn,omitempty"`
404+
}
405+
406+
type FacetSearchResponse struct {
407+
FacetHits []interface{} `json:"facetHits"`
408+
FacetQuery string `json:"facetQuery"`
409+
ProcessingTimeMs int64 `json:"processingTimeMs"`
410+
}
411+
397412
// DocumentQuery is the request body get one documents method
398413
type DocumentQuery struct {
399414
Fields []string `json:"fields,omitempty"`

0 commit comments

Comments
 (0)