Skip to content

Commit

Permalink
Feature/facets (#16)
Browse files Browse the repository at this point in the history
* Add faceted configuration

* Add facets on Changelog

* Replace String with [[String]]

* Correcting facetFilter encode and remove Filter type to support multiple fields in the filter

* Check the length of the parameters before building the dictionary

* Fix wrong name of parameter in the faceted attributes attribute

Co-authored-by: Samuel Jimenez <[email protected]>

* Update Sources/MeiliSearch/Client.swift

Co-authored-by: Samuel Jimenez <[email protected]>

* Update Sources/MeiliSearch/Client.swift

Co-authored-by: Samuel Jimenez <[email protected]>

* Update Sources/MeiliSearch/Client.swift

Co-authored-by: Samuel Jimenez <[email protected]>

* Update Sources/MeiliSearch/Client.swift

Co-authored-by: Samuel Jimenez <[email protected]>

Co-authored-by: Samuel Jimenez <[email protected]>
  • Loading branch information
ppamorim and eskombro authored Jul 20, 2020
1 parent 6110149 commit eee8421
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## vX.X.X

- Added getOrCreateIndex function (#4)
- Added Facets support (#15)
46 changes: 46 additions & 0 deletions Sources/MeiliSearch/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,52 @@ public struct MeiliSearch {
self.settings.updateAcceptNewFields(UID, acceptNewFields, completion)
}

// MARK: Attributes for faceting

/**
Get the attributes selected to be faceted of an `Index`.
- parameter UID: The unique identifier for the `Index` to be found.
- parameter completion: The completion closure used to notify when the server
completes the query request, it returns a `Result` object that contains an `[String]`
value if the request was successful, or `Error` if a failure occurred.
*/
public func getAttributesForFaceting(
UID: String,
_ completion: @escaping (Result<[String], Swift.Error>) -> Void) {
self.settings.getAttributesForFaceting(UID, completion)
}

/**
Update the faceted attributes of an `Index`.
- parameter UID: The unique identifier for the `Index` to be found.
- parameter attributes: The faceted attributes to be applied into `Index`.
- parameter completion: The completion closure used to notify when the server
completes the query request, it returns a `Result` object that contains `Update`
value if the request was successful, or `Error` if a failure occurred.
*/
public func updateAttributesForFaceting(
UID: String,
_ attributes: [String],
_ completion: @escaping (Result<Update, Swift.Error>) -> Void) {
self.settings.updateAttributesForFaceting(UID, attributes, completion)
}

/**
Reset the faceted attributes of an `Index`.
- parameter UID: The unique identifier for the `Index` to be reset.
- parameter completion: The completion closure used to notify when the server
completes the query request, it returns a `Result` object that contains `Update`
value if the request was successful, or `Error` if a failure occurred.
*/
public func resetAttributesForFaceting(
UID: String,
_ completion: @escaping (Result<Update, Swift.Error>) -> Void) {
self.settings.resetAttributesForFaceting(UID, completion)
}

// MARK: Stats

/**
Expand Down
102 changes: 62 additions & 40 deletions Sources/MeiliSearch/Model/SearchParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ public struct SearchParameters: Codable, Equatable {
public let attributesToHighlight: [String]

/// Attribute with an exact match.
public let filters: Filter?
public let filters: String?

/// Select which attribute has to be filtered, useful when you need to narrow down the results of the filter.
//TODO: Migrate to FacetFilter object
public let facetFilters: [[String]]?

/// Retrieve the count of matching terms for each facets.
public let facetsDistribution: [String]?

/// Whether to return the raw matches or not.
public let matches: Bool
Expand All @@ -46,7 +53,9 @@ public struct SearchParameters: Codable, Equatable {
attributesToCrop: [String] = [],
cropLength: Int = 200,
attributesToHighlight: [String] = [],
filters: Filter? = nil,
filters: String? = nil,
facetFilters: [[String]]? = nil,
facetsDistribution: [String]? = nil,
matches: Bool = false) {
self.query = query
self.offset = offset
Expand All @@ -56,6 +65,8 @@ public struct SearchParameters: Codable, Equatable {
self.cropLength = cropLength
self.attributesToHighlight = attributesToHighlight
self.filters = filters
self.facetFilters = facetFilters
self.facetsDistribution = facetsDistribution
self.matches = matches
}

Expand All @@ -72,50 +83,61 @@ public struct SearchParameters: Codable, Equatable {
}

func dictionary() -> [String: String] {
var dic = [String: String]()
dic["q"] = query
dic["offset"] = "\(offset)"
dic["limit"] = "\(limit)"

if let attributesToRetrieve = self.attributesToRetrieve {
dic["attributesToRetrieve"] = commaRepresentation(attributesToRetrieve)
}

if !attributesToCrop.isEmpty {
dic["attributesToCrop"] = commaRepresentation(attributesToCrop)
}

dic["cropLength"] = "\(cropLength)"

if !attributesToHighlight.isEmpty {
dic["attributesToHighlight"] = commaRepresentation(attributesToHighlight)
}

if let filters: Filter = self.filters {
dic["filters"] = "\(filters.attribute):\(filters.value)"
}

dic["matches"] = "\(matches)"
return dic
var dic = [String: String]()

dic["q"] = query
dic["offset"] = "\(offset)"
dic["limit"] = "\(limit)"

if let attributesToRetrieve = self.attributesToRetrieve, !attributesToRetrieve.isEmpty {
dic["attributesToRetrieve"] = commaRepresentation(attributesToRetrieve)
}

if !attributesToCrop.isEmpty {
dic["attributesToCrop"] = commaRepresentation(attributesToCrop)
}

dic["cropLength"] = "\(cropLength)"

if !attributesToHighlight.isEmpty {
dic["attributesToHighlight"] = commaRepresentation(attributesToHighlight)
}

if let filters: String = self.filters, !filters.isEmpty {
dic["filters"] = filters
}

if let facetFilters: [[String]] = self.facetFilters, !facetFilters.isEmpty {
var value = "["
for (index, facetFilter) in facetFilters.enumerated() {
let entry = commaRepresentationEscaped(facetFilter)
value += entry
if index < facetFilters.count - 1 {
value += ","
}
}
value += "]"
dic["facetFilters"] = value
}

if let facetsDistribution: [String] = self.facetsDistribution, !facetsDistribution.isEmpty {
dic["facetsDistribution"] = commaRepresentationEscaped(facetsDistribution)
}

dic["matches"] = "\(matches)"
return dic
}

private func commaRepresentation(_ array: [String]) -> String {
array.joined(separator: ",")
array.joined(separator: ",")
}

/**
`Filter` instances represent filter used in thr search query.
*/
public struct Filter: Codable, Equatable {

// MARK: Properties

/// Parameter from document to be filtered.
let attribute: String

/// Value of the document parameter to be filtered.
let value: String

private func commaRepresentationEscaped(_ array: [String]) -> String {
var value: String = "["
value += array.map({ string in "\"\(string)\"" }).joined(separator: ",")
value += "]"
return value
}

}
96 changes: 96 additions & 0 deletions Sources/MeiliSearch/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -741,4 +741,100 @@ struct Settings {

}

// MARK: Attributes for faceting

func getAttributesForFaceting(
_ UID: String,
_ completion: @escaping (Result<[String], Swift.Error>) -> Void) {

self.request.get(api: "/indexes/\(UID)/settings/attributes-for-faceting") { result in

switch result {
case .success(let data):

guard let data: Data = data else {
completion(.failure(MeiliSearch.Error.dataNotFound))
return
}

do {
let dictionary: [String] = try JSONSerialization
.jsonObject(with: data, options: []) as! [String]
completion(.success(dictionary))
} catch {
completion(.failure(error))
}

case .failure(let error):
completion(.failure(error))
}

}

}

func updateAttributesForFaceting(
_ UID: String,
_ attributes: [String],
_ completion: @escaping (Result<Update, Swift.Error>) -> Void) {

let data: Data
do {
data = try JSONSerialization.data(withJSONObject: attributes, options: [])
} catch {
completion(.failure(error))
return
}

self.request.post(api: "/indexes/\(UID)/settings/attributes-for-faceting", data) { result in

switch result {
case .success(let data):

do {
let decoder: JSONDecoder = JSONDecoder()
let update: Update = try decoder.decode(Update.self, from: data)
completion(.success(update))
} catch {
completion(.failure(error))
}

case .failure(let error):
completion(.failure(error))
}

}

}

func resetAttributesForFaceting(
_ UID: String,
_ completion: @escaping (Result<Update, Swift.Error>) -> Void) {

self.request.delete(api: "/indexes/\(UID)/settings/attributes-for-faceting") { result in

switch result {
case .success(let data):

guard let data: Data = data else {
completion(.failure(MeiliSearch.Error.dataNotFound))
return
}

do {
let decoder: JSONDecoder = JSONDecoder()
let update: Update = try decoder.decode(Update.self, from: data)
completion(.success(update))
} catch {
completion(.failure(error))
}

case .failure(let error):
completion(.failure(error))
}

}

}

}
65 changes: 63 additions & 2 deletions Tests/MeiliSearchTests/SearchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,69 @@ class SearchTests: XCTestCase {
self.client.search(UID: uid, searchParameters) { (result: MeiliResult) in
switch result {
case .success(let searchResult):

XCTAssertEqual(stubSearchResult, searchResult)
expectation.fulfill()
case .failure:
XCTFail("Failed to search for botman")
}
}

self.wait(for: [expectation], timeout: 1.0)

}

func testSearchForBotmanMovieFacets() {

//Prepare the mock server

let jsonString = """
{
"hits": [
{
"id": 29751,
"title": "Batman Unmasked: The Psychology of the Dark Knight",
"poster": "https://image.tmdb.org/t/p/w1280/jjHu128XLARc2k4cJrblAvZe0HE.jpg",
"overview": "Delve into the world of Batman and the vigilante justice tha",
"release_date": "2020-04-04T19:59:49.259572Z"
},
{
"id": 471474,
"title": "Batman: Gotham by Gaslight",
"poster": "https://image.tmdb.org/t/p/w1280/7souLi5zqQCnpZVghaXv0Wowi0y.jpg",
"overview": "ve Victorian Age Gotham City, Batman begins his war on crime",
"release_date": "2020-04-04T19:59:49.259572Z"
}
],
"offset": 0,
"limit": 20,
"processingTimeMs": 2,
"query": "botman"
}
"""

let decoder: JSONDecoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(Formatter.iso8601)
let data = jsonString.data(using: .utf8)!
let stubSearchResult: SearchResult<Movie> = try! decoder.decode(SearchResult<Movie>.self, from: data)

session.pushData(jsonString)

// Start the test with the mocked server

let uid: String = "Movies"

let searchParameters = SearchParameters(
query: "botman",
facetFilters: [["genre:romance"], ["genre:action"]])

let expectation = XCTestExpectation(description: "Searching for botman")

typealias MeiliResult = Result<SearchResult<Movie>, Swift.Error>

self.client.search(UID: uid, searchParameters) { (result: MeiliResult) in
switch result {
case .success(let searchResult):
XCTAssertEqual(stubSearchResult, searchResult)
expectation.fulfill()
case .failure:
XCTFail("Failed to search for botman")
Expand All @@ -92,7 +152,8 @@ class SearchTests: XCTestCase {
}

static var allTests = [
("testSearchForBotmanMovie", testSearchForBotmanMovie)
("testSearchForBotmanMovie", testSearchForBotmanMovie),
("testSearchForBotmanMovieFacets", testSearchForBotmanMovieFacets)
]

}
Loading

0 comments on commit eee8421

Please sign in to comment.