Skip to content

Commit

Permalink
Merge pull request #39 from meilisearch/implement_faceting
Browse files Browse the repository at this point in the history
Implement faceting
  • Loading branch information
eskombro authored Jun 12, 2020
2 parents 6f4af32 + 75d9d8a commit ecb785e
Show file tree
Hide file tree
Showing 7 changed files with 472 additions and 29 deletions.
6 changes: 6 additions & 0 deletions apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ type APISettings interface {
GetAcceptNewFields() (*bool, error)

UpdateAcceptNewFields(bool) (*AsyncUpdateID, error)

GetAttributesForFaceting() (*[]string, error)

UpdateAttributesForFaceting([]string) (*AsyncUpdateID, error)

ResetAttributesForFaceting() (*AsyncUpdateID, error)
}

// APIStats retrieve statistic over all indexes or a specific index id.
Expand Down
6 changes: 6 additions & 0 deletions client_documents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ type docTest struct {
Name string `json:"name"`
}

type docTestBooks struct {
BookID int `json:"book_id"`
Title string `json:"title"`
Tag string `json:"tag"`
}

func TestClientDocuments_Get(t *testing.T) {
var indexUID = "TestClientDocuments_Get"

Expand Down
32 changes: 32 additions & 0 deletions client_search.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package meilisearch

import (
"fmt"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -51,6 +52,13 @@ func (c clientSearch) Search(request SearchRequest) (*SearchResponse, error) {
if request.Matches {
values.Add("matches", strconv.FormatBool(request.Matches))
}
if len(request.FacetsDistribution) != 0 {
values.Add("facetsDistribution", fmt.Sprintf("[%q]", strings.Join(request.FacetsDistribution, "\",\"")))
}
if request.FacetFilters != nil {
facetFiltersToStr := facetFiltersToStr(request.FacetFilters)
values.Add("facetFilters", facetFiltersToStr)
}

req := internalRequest{
endpoint: "/indexes/" + c.indexUID + "/search?" + values.Encode(),
Expand All @@ -76,3 +84,27 @@ func (c clientSearch) IndexID() string {
func (c clientSearch) Client() *Client {
return c.client
}

func facetFiltersToStr(i interface{}) string {
facetsToStr := ""
switch v := i.(type) {
case []string:
for _, slice := range v {
stringSlice := slice
facetsToStr += fmt.Sprintf("%q,", stringSlice)
}
facetsToStr = fmt.Sprintf("[%s]", facetsToStr[:len(facetsToStr)-1])
case [][]string:
for _, mainSlice := range v {
facetsToStr += "["
for _, slice := range mainSlice {
stringSlice := slice
facetsToStr += fmt.Sprintf("%q,", stringSlice)
}
facetsToStr = facetsToStr[:len(facetsToStr)-1] + "],"

}
facetsToStr = fmt.Sprintf("[%s]", facetsToStr[:len(facetsToStr)-1])
}
return (facetsToStr)
}
249 changes: 242 additions & 7 deletions client_search_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package meilisearch

import (
"fmt"
"testing"
)

Expand All @@ -19,30 +20,264 @@ func TestClientSearch_Search(t *testing.T) {
t.Fatal(err)
}

booksTest := []docTestBooks{
{BookID: 123, Title: "Pride and Prejudice", Tag: "Nice book"},
{BookID: 456, Title: "Le Petit Prince", Tag: "Nice book"},
{BookID: 1, Title: "Alice In Wonderland", Tag: "Nice book"},
{BookID: 1344, Title: "The Hobbit", Tag: "Nice book"},
{BookID: 4, Title: "Harry Potter and the Half-Blood Prince", Tag: "Interesting book"},
{BookID: 42, Title: "The Hitchhiker's Guide to the Galaxy", Tag: "Interesting book"},
{BookID: 24, Title: "You are a princess", Tag: "Interesting book"},
}

updateIDRes, err := client.
Documents(indexUID).
AddOrUpdate([]docTest{
{"0", "J'adore les citrons"},
{"1", "Les citrons c'est la vie"},
{"2", "Les ponchos c'est bien !"},
})
AddOrUpdate(booksTest)

if err != nil {
t.Fatal(err)
}

client.DefaultWaitForPendingUpdate(indexUID, updateIDRes)

// Test basic search

resp, err := client.Search(indexUID).Search(SearchRequest{
Query: "citrons",
Limit: 10,
Query: "prince",
})

if err != nil {
t.Fatal(err)
}

if len(resp.Hits) != 3 {
fmt.Println(resp)
t.Fatal("Basic search: number of hits should be equal to 3")
}
title := resp.Hits[0].(map[string]interface{})["title"]
if title != booksTest[1].Title {
fmt.Println(resp)
t.Fatalf("Basic search: should have found %s\n", booksTest[1].Title)
}

// Test basic search with limit

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
Limit: 1,
})

if err != nil {
t.Fatal(err)
}

if len(resp.Hits) != 1 {
fmt.Println(resp)
t.Fatal("Search offset: number of hits should be equal to 1")
}
title = resp.Hits[0].(map[string]interface{})["title"]
if title != booksTest[1].Title {
fmt.Println(resp)
t.Fatalf("Basic search: should have found %s\n", booksTest[1].Title)
}

// Test basic search with offset

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
Offset: 1,
})

if err != nil {
t.Fatal(err)
}

if len(resp.Hits) != 2 {
fmt.Println(resp)
t.Fatal("number of hits should be equal to 2")
}
retrievedTitles := []string{
fmt.Sprint(resp.Hits[0].(map[string]interface{})["title"]),
fmt.Sprint(resp.Hits[1].(map[string]interface{})["title"]),
}
expectedTitles := []string{
booksTest[4].Title,
booksTest[6].Title,
}

for title := range expectedTitles {
found := false
for retrievedTitle := range retrievedTitles {
if title == retrievedTitle {
found = true
break
}
}
if !found {
fmt.Println(resp)
t.Fatal("Search offset: should have found 'Harry Potter and the Half-Blood Prince'")
}
}

// Test basic search with attributesToRetrieve

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
AttributesToRetrieve: []string{"book_id", "title"},
})

if err != nil {
t.Fatal(err)
}

if resp.Hits[0].(map[string]interface{})["title"] == nil {
fmt.Println(resp)
t.Fatal("attributesToRetrieve: Couldn't retrieve field in response")
}
if resp.Hits[0].(map[string]interface{})["tag"] != nil {
fmt.Println(resp)
t.Fatal("attributesToRetrieve: Retrieve unrequested field in response")
}

// Test basic search with attributesToCrop

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "to",
AttributesToCrop: []string{"title"},
CropLength: 7,
})

if err != nil {
t.Fatal(err)
}

if resp.Hits[0].(map[string]interface{})["title"] == nil {
fmt.Println(resp)
t.Fatal("attributesToCrop: Couldn't retrieve field in response")
}
formatted := resp.Hits[0].(map[string]interface{})["_formatted"]
if formatted.(map[string]interface{})["title"] != "Guide to the" {
fmt.Println(resp)
t.Fatal("attributesToCrop: CropLength didn't work as expected")
}

// Test basic search with filters

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "and",
Filters: "tag = \"Nice book\"",
})

if err != nil {
t.Fatal(err)
}

if len(resp.Hits) != 1 {
fmt.Println(resp)
t.Fatal("filters: Unable to filter properly")
}
if resp.Hits[0].(map[string]interface{})["title"] != "Pride and Prejudice" {
fmt.Println(resp)
t.Fatal("filters: Unable to filter properly")
}

// Test basic search with matches

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "and",
Matches: true,
})

if err != nil {
t.Fatal(err)
}

if resp.Hits[0].(map[string]interface{})["_matchesInfo"] == nil {
fmt.Println(resp)
t.Fatal("matches: Mathes info not found")
}

// Test basic search with facetsDistribution

r2, err := client.Settings(indexUID).UpdateAttributesForFaceting([]string{"tag"})

if err != nil {
t.Fatal(err)
}

client.DefaultWaitForPendingUpdate(indexUID, r2)

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
FacetsDistribution: []string{"*"},
})

if err != nil {
t.Fatal(err)
}

tagCount := resp.FacetsDistribution.(map[string]interface{})["tag"]

if len(tagCount.(map[string]interface{})) != 2 {
fmt.Println(tagCount.(map[string]interface{}))
t.Fatal("facetsDistribution: Wrong count of facet options")
}

if tagCount.(map[string]interface{})["interesting book"] != float64(2) {
fmt.Println(tagCount.(map[string]interface{})["interesting book"])
t.Fatal("facetsDistribution: Wrong count on facetDistribution")
}

r2, _ = client.Settings(indexUID).ResetAttributesForFaceting()
client.DefaultWaitForPendingUpdate(indexUID, r2)

// Test basic search with facetFilters

r2, err = client.Settings(indexUID).UpdateAttributesForFaceting([]string{"tag", "title"})

if err != nil {
t.Fatal(err)
}

client.DefaultWaitForPendingUpdate(indexUID, r2)

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
FacetFilters: []string{"tag:interesting book"},
})
if err != nil {
fmt.Println("Error:", err)
}

if len(resp.Hits) != 2 {
fmt.Println(resp)
t.Fatal("facetsFilters: Error on single attribute facet search")
}

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
FacetFilters: []string{"tag:interesting book", "tag:nice book"},
})
if err != nil {
fmt.Println("Error:", err)
}

if len(resp.Hits) != 0 {
fmt.Println(resp)
t.Fatal("facetsFilters: Error on 'AND' in attribute facet search")
}

resp, err = client.Search(indexUID).Search(SearchRequest{
Query: "prince",
FacetFilters: [][]string{{"tag:interesting book", "tag:nice book"}},
})
if err != nil {
fmt.Println("Error:", err)
}

if len(resp.Hits) != 3 {
fmt.Println(resp)
t.Fatal("facetsFilters: Error on 'OR' in attribute facet search")
}

}
Loading

0 comments on commit ecb785e

Please sign in to comment.