Skip to content

Commit

Permalink
Merge #543
Browse files Browse the repository at this point in the history
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 <miguel@games.com>
Co-authored-by: Clémentine <clementine@meilisearch.com>
  • Loading branch information
3 people authored Jul 29, 2024
2 parents aea59d3 + c4e85d6 commit 26be7c8
Show file tree
Hide file tree
Showing 5 changed files with 790 additions and 206 deletions.
15 changes: 15 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ multi_search_1: |-
},
},
})
facet_search_1: |-
client.Index("books").FacetSearch(&meilisearch.FacetSearchRequest{
FacetQuery: "fiction",
FacetName: "genres",
Filter: "rating > 3",
})
facet_search_3: |-
client.Index("books").FacetSearch(&meilisearch.FacetSearchRequest{
FacetQuery: "c",
FacetName: "genres",
})
delete_tasks_1: |-
client.DeleteTaks(&meilisearch.DeleteTasksQuery{
UIDS: []int64{1, 2},
Expand Down Expand Up @@ -645,7 +656,11 @@ getting_started_configure_settings: |-
getting_started_faceting: |-
client.Index("movies").UpdateFaceting(&meilisearch.Faceting{
MaxValuesPerFacet: 2,
SortFacetValuesBy: {
"*": "count"
}
})
getting_started_pagination: |-
client.Index("movies").UpdatePagination(&meilisearch.Pagination{
MaxTotalHits: 500,
Expand Down
60 changes: 60 additions & 0 deletions index_facet_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package meilisearch

import (
"encoding/json"
"errors"
"net/http"
)

var ErrNoFacetSearchRequest = errors.New("no search facet request provided")

func (i Index) FacetSearch(request *FacetSearchRequest) (*json.RawMessage, error) {
if request == nil {
return nil, ErrNoFacetSearchRequest
}

searchPostRequestParams := FacetSearchPostRequestParams(request)

resp := &json.RawMessage{}

req := internalRequest{
endpoint: "/indexes/" + i.UID + "/facet-search",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: searchPostRequestParams,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "FacetSearch",
}

if err := i.client.executeRequest(req); err != nil {
return nil, err
}

return resp, nil
}

func FacetSearchPostRequestParams(request *FacetSearchRequest) map[string]interface{} {
params := make(map[string]interface{}, 22)

if request.Q != "" {
params["q"] = request.Q
}
if request.FacetName != "" {
params["facetName"] = request.FacetName
}
if request.FacetQuery != "" {
params["facetQuery"] = request.FacetQuery
}
if request.Filter != "" {
params["filter"] = request.Filter
}
if request.MatchingStrategy != "" {
params["matchingStrategy"] = request.MatchingStrategy
}
if len(request.AttributesToSearchOn) != 0 {
params["attributesToSearchOn"] = request.AttributesToSearchOn
}

return params
}
213 changes: 213 additions & 0 deletions index_facet_search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package meilisearch

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestIndex_FacetSearch(t *testing.T) {
type args struct {
UID string
PrimaryKey string
client *Client
request *FacetSearchRequest
filterableAttributes []string
}

tests := []struct {
name string
args args
want *FacetSearchResponse
wantErr bool
}{
{
name: "TestIndexBasicFacetSearch",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetName: "tag",
FacetQuery: "Novel",
},
filterableAttributes: []string{"tag"},
},
want: &FacetSearchResponse{
FacetHits: []interface{}{
map[string]interface{}{
"value": "Novel", "count": float64(5),
},
},
FacetQuery: "Novel",
},
wantErr: false,
},
{
name: "TestIndexFacetSearchWithFilter",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetName: "tag",
FacetQuery: "Novel",
Filter: "tag = 'Novel'",
},
filterableAttributes: []string{"tag"},
},
want: &FacetSearchResponse{
FacetHits: []interface{}{
map[string]interface{}{
"value": "Novel", "count": float64(5),
},
},
FacetQuery: "Novel",
},
wantErr: false,
},
{
name: "TestIndexFacetSearchWithMatchingStrategy",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetName: "tag",
FacetQuery: "Novel",
MatchingStrategy: "frequency",
},
filterableAttributes: []string{"tag"},
},
want: &FacetSearchResponse{
FacetHits: []interface{}{
map[string]interface{}{
"value": "Novel", "count": float64(5),
},
},
FacetQuery: "Novel",
},
wantErr: false,
},
{
name: "TestIndexFacetSearchWithAttributesToSearchOn",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetName: "tag",
FacetQuery: "Novel",
AttributesToSearchOn: []string{"tag"},
},
filterableAttributes: []string{"tag"},
},
want: &FacetSearchResponse{
FacetHits: []interface{}{
map[string]interface{}{
"value": "Novel", "count": float64(5),
},
},
FacetQuery: "Novel",
},
wantErr: false,
},
{
name: "TestIndexFacetSearchWithNoFacetSearchRequest",
args: args{
UID: "indexUID",
client: defaultClient,
request: nil,
},
want: nil,
wantErr: true,
},
{
name: "TestIndexFacetSearchWithNoFacetName",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetQuery: "Novel",
},
},
want: nil,
wantErr: true,
},
{
name: "TestIndexFacetSearchWithNoFacetQuery",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetName: "tag",
},
},
want: nil,
wantErr: true,
},
{
name: "TestIndexFacetSearchWithNoFilterableAttributes",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
FacetName: "tag",
FacetQuery: "Novel",
},
},
want: nil,
wantErr: true,
},
{
name: "TestIndexFacetSearchWithQ",
args: args{
UID: "indexUID",
client: defaultClient,
request: &FacetSearchRequest{
Q: "query",
FacetName: "tag",
},
filterableAttributes: []string{"tag"},
},
want: &FacetSearchResponse{
FacetHits: []interface{}{},
FacetQuery: "",
},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
SetUpIndexForFaceting()
c := tt.args.client
i := c.Index(tt.args.UID)
t.Cleanup(cleanup(c))

if len(tt.args.filterableAttributes) > 0 {
updateFilter, err := i.UpdateFilterableAttributes(&tt.args.filterableAttributes)
require.NoError(t, err)
testWaitForTask(t, i, updateFilter)
}

gotRaw, err := i.FacetSearch(tt.args.request)

if tt.wantErr {
require.Error(t, err)
require.Nil(t, gotRaw)
return
}

require.NoError(t, err)
// Unmarshall the raw response from FacetSearch into a FacetSearchResponse
var got FacetSearchResponse
err = json.Unmarshal(*gotRaw, &got)
require.NoError(t, err, "error unmarshalling raw got FacetSearchResponse")

require.Equal(t, len(tt.want.FacetHits), len(got.FacetHits))
for len := range got.FacetHits {
require.Equal(t, tt.want.FacetHits[len].(map[string]interface{})["value"], got.FacetHits[len].(map[string]interface{})["value"])
require.Equal(t, tt.want.FacetHits[len].(map[string]interface{})["count"], got.FacetHits[len].(map[string]interface{})["count"])
}
require.Equal(t, tt.want.FacetQuery, got.FacetQuery)
})
}
}
15 changes: 15 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,21 @@ type MultiSearchResponse struct {
Results []SearchResponse `json:"results"`
}

type FacetSearchRequest struct {
FacetName string `json:"facetName,omitempty"`
FacetQuery string `json:"facetQuery,omitempty"`
Q string `json:"q,omitempty"`
Filter string `json:"filter,omitempty"`
MatchingStrategy string `json:"matchingStrategy,omitempty"`
AttributesToSearchOn []string `json:"attributesToSearchOn,omitempty"`
}

type FacetSearchResponse struct {
FacetHits []interface{} `json:"facetHits"`
FacetQuery string `json:"facetQuery"`
ProcessingTimeMs int64 `json:"processingTimeMs"`
}

// DocumentQuery is the request body get one documents method
type DocumentQuery struct {
Fields []string `json:"fields,omitempty"`
Expand Down
Loading

0 comments on commit 26be7c8

Please sign in to comment.