Skip to content

Commit

Permalink
Merge #411
Browse files Browse the repository at this point in the history
411: Add typo-tolerance APIs r=curquiza a=Sherlouk

# Pull Request

## Related issue
Fixes #282 

## What does this PR do?
- Adds support for reading, updating, and resetting typo-tolerance preferences

## 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: James Sherlock <15193942+Sherlouk@users.noreply.github.com>
  • Loading branch information
meili-bors[bot] and Sherlouk authored Sep 27, 2023
2 parents e3ba51a + a5cacc9 commit 827adb4
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 7 deletions.
40 changes: 40 additions & 0 deletions Sources/MeiliSearch/Indexes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,46 @@ public struct Indexes {
self.settings.resetPaginationSettings(self.uid, completion)
}

// MARK: Typo Tolerance

/**
Get the typo tolerance settings.

- parameter completion: The completion closure is used to notify when the server
completes the query request, it returns a `Result` object that contains a `TypoToleranceResult`
value if the request was successful, or `Error` if a failure occurred.
*/
public func getTypoTolerance(
_ completion: @escaping (Result<TypoToleranceResult, Swift.Error>) -> Void) {
self.settings.getTypoTolerance(self.uid, completion)
}

/**
Update the typo tolerance settings.

- parameter typoTolerance: An object containing the settings for the `Index`.
- parameter completion: The completion closure is used to notify when the server
completes the query request, it returns a `Result` object that contains `TaskInfo`
value if the request was successful, or `Error` if a failure occurred.
*/
public func updateTypoTolerance(
_ typoTolerance: TypoTolerance,
_ completion: @escaping (Result<TaskInfo, Swift.Error>) -> Void) {
self.settings.updateTypoTolerance(self.uid, typoTolerance, completion)
}

/**
Reset the typo tolerance settings.

- parameter completion: The completion closure is used to notify when the server
completes the query request, it returns a `Result` object that contains `TaskInfo`
value if the request was successful, or `Error` if a failure occurred.
*/
public func resetTypoTolerance(
_ completion: @escaping (Result<TaskInfo, Swift.Error>) -> Void) {
self.settings.resetTypoTolerance(self.uid, completion)
}

// MARK: Stats

/**
Expand Down
9 changes: 7 additions & 2 deletions Sources/MeiliSearch/Model/Setting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Foundation
#endif

/**
Settings object provided byb the user
Settings object provided by the user
*/
public struct Setting: Codable, Equatable {
// MARK: Properties
Expand Down Expand Up @@ -33,6 +33,9 @@ public struct Setting: Codable, Equatable {
/// List of attributes used for sorting
public let sortableAttributes: [String]?

/// Settings for typo tolerance
public let typoTolerance: TypoTolerance?

/// List of tokens that will be considered as word separators by Meilisearch.
public let separatorTokens: [String]?

Expand All @@ -59,7 +62,8 @@ public struct Setting: Codable, Equatable {
separatorTokens: [String]? = nil,
nonSeparatorTokens: [String]? = nil,
dictionary: [String]? = nil,
pagination: Pagination? = nil
pagination: Pagination? = nil,
typoTolerance: TypoTolerance? = nil
) {
self.rankingRules = rankingRules
self.searchableAttributes = searchableAttributes
Expand All @@ -73,5 +77,6 @@ public struct Setting: Codable, Equatable {
self.separatorTokens = separatorTokens
self.dictionary = dictionary
self.pagination = pagination
self.typoTolerance = typoTolerance
}
}
3 changes: 3 additions & 0 deletions Sources/MeiliSearch/Model/SettingResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ public struct SettingResult: Codable, Equatable {

/// Pagination settings for the current index
public let pagination: Pagination

/// Settings for typo tolerance
public let typoTolerance: TypoToleranceResult
}
2 changes: 2 additions & 0 deletions Sources/MeiliSearch/Model/Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public struct Task: Codable, Equatable {
/// Settings for index level pagination rules
public let pagination: Pagination?

/// Typo tolerance on settings actions
public let typoTolerance: TypoTolerance?
}
/// Error information in case of failed update.
public let error: MeiliSearch.MSErrorResponse?
Expand Down
48 changes: 48 additions & 0 deletions Sources/MeiliSearch/Model/TypoTolerance.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

/**
`TypoTolerance` settings provided by the user.
*/
public struct TypoTolerance: Codable, Equatable {
// MARK: Properties

/// Whether typo tolerance is enabled or not
public let enabled: Bool?

/// The minimum word size for accepting typos
public let minWordSizeForTypos: MinWordSize?

/// An array of words for which the typo tolerance feature is disabled
public let disableOnWords: [String]?

/// An array of attributes for which the typo tolerance feature is disabled
public let disableOnAttributes: [String]?

public struct MinWordSize: Codable, Equatable {
/// The minimum word size for accepting 1 typo; must be between 0 and `twoTypos`
public let oneTypo: Int?

/// The minimum word size for accepting 2 typos; must be between `oneTypo` and 255
public let twoTypos: Int?

public init(
oneTypo: Int? = nil,
twoTypos: Int? = nil
) {
self.oneTypo = oneTypo
self.twoTypos = twoTypos
}
}

public init(
enabled: Bool? = nil,
minWordSizeForTypos: TypoTolerance.MinWordSize? = nil,
disableOnWords: [String]? = nil,
disableOnAttributes: [String]? = nil
) {
self.enabled = enabled
self.minWordSizeForTypos = minWordSizeForTypos
self.disableOnWords = disableOnWords
self.disableOnAttributes = disableOnAttributes
}
}
28 changes: 28 additions & 0 deletions Sources/MeiliSearch/Model/TypoToleranceResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

/**
`TypoToleranceResult` instances represent the current typo tolerance settings.
*/
public struct TypoToleranceResult: Codable, Equatable {
// MARK: Properties

/// Whether typo tolerance is enabled or not
public let enabled: Bool

/// The minimum word size for accepting typos
public let minWordSizeForTypos: MinWordSize

/// An array of words for which the typo tolerance feature is disabled
public let disableOnWords: [String]

/// An array of attributes for which the typo tolerance feature is disabled
public let disableOnAttributes: [String]

public struct MinWordSize: Codable, Equatable {
/// The minimum word size for accepting 1 typo; must be between 0 and `twoTypos`
public let oneTypo: Int

/// The minimum word size for accepting 2 typos; must be between `oneTypo` and 255
public let twoTypos: Int
}
}
45 changes: 45 additions & 0 deletions Sources/MeiliSearch/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,51 @@ struct Settings {
resetSetting(uid: uid, key: "pagination", completion: completion)
}

// MARK: Typo Tolerance

func getTypoTolerance(
_ uid: String,
_ completion: @escaping (Result<TypoToleranceResult, Swift.Error>) -> Void) {

getSetting(uid: uid, key: "typo-tolerance", completion: completion)
}

func updateTypoTolerance(
_ uid: String,
_ setting: TypoTolerance,
_ completion: @escaping (Result<TaskInfo, Swift.Error>) -> Void) {

let data: Data
do {
data = try JSONEncoder().encode(setting)
} catch {
completion(.failure(error))
return
}

// this uses patch instead of put for networking, so shouldn't use the reusable 'updateSetting' function
self.request.patch(api: "/indexes/\(uid)/settings/typo-tolerance", data) { result in
switch result {
case .success(let data):
do {
let task: TaskInfo = try Constants.customJSONDecoder.decode(TaskInfo.self, from: data)
completion(.success(task))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}

func resetTypoTolerance(
_ uid: String,
_ completion: @escaping (Result<TaskInfo, Swift.Error>) -> Void) {

resetSetting(uid: uid, key: "typo-tolerance", completion: completion)
}

// MARK: Reusable Requests

private func getSetting<ResponseType: Decodable>(
Expand Down
121 changes: 116 additions & 5 deletions Tests/MeiliSearchIntegrationTests/SettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ class SettingsTests: XCTestCase {
private let defaultStopWords: [String] = []
private let defaultSynonyms: [String: [String]] = [:]
private let defaultPagination: Pagination = .init(maxTotalHits: 1000)
private let defaultTypoTolerance: TypoTolerance = .init(
enabled: true,
minWordSizeForTypos: .init(oneTypo: 5, twoTypos: 9),
disableOnWords: [],
disableOnAttributes: []
)
private let defaultTypoToleranceResult: TypoToleranceResult = .init(
enabled: true,
minWordSizeForTypos: .init(oneTypo: 5, twoTypos: 9),
disableOnWords: [],
disableOnAttributes: []
)
private var defaultGlobalSettings: Setting?
private var defaultGlobalReturnedSettings: SettingResult?

Expand Down Expand Up @@ -68,7 +80,8 @@ class SettingsTests: XCTestCase {
separatorTokens: self.defaultSeparatorTokens,
nonSeparatorTokens: self.defaultNonSeparatorTokens,
dictionary: self.defaultDictionary,
pagination: self.defaultPagination
pagination: self.defaultPagination,
typoTolerance: self.defaultTypoTolerance
)

self.defaultGlobalReturnedSettings = SettingResult(
Expand All @@ -83,7 +96,8 @@ class SettingsTests: XCTestCase {
separatorTokens: self.defaultSeparatorTokens,
nonSeparatorTokens: self.defaultNonSeparatorTokens,
dictionary: self.defaultDictionary,
pagination: self.defaultPagination
pagination: self.defaultPagination,
typoTolerance: self.defaultTypoToleranceResult
)
}

Expand Down Expand Up @@ -525,6 +539,100 @@ class SettingsTests: XCTestCase {
self.wait(for: [expectation], timeout: TESTS_TIME_OUT)
}

// MARK: Typo Tolerance

func testGetTypoTolerance() {
let expectation = XCTestExpectation(description: "Get current typo tolerance")

self.index.getTypoTolerance { result in
switch result {
case .success(let typoTolerance):
XCTAssertEqual(self.defaultTypoToleranceResult, typoTolerance)
expectation.fulfill()
case .failure(let error):
dump(error)
XCTFail("Failed to get typo tolerance")
expectation.fulfill()
}
}

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

func testUpdateTypoTolerance() {
let expectation = XCTestExpectation(description: "Update settings for typo tolerance")

let newTypoTolerance: TypoTolerance = .init(
minWordSizeForTypos: .init(
oneTypo: 1,
twoTypos: 2
),
disableOnWords: ["to"],
disableOnAttributes: ["genre"]
)

self.index.updateTypoTolerance(newTypoTolerance) { result in
switch result {
case .success(let task):
self.client.waitForTask(task: task) { result in
switch result {
case .success(let task):
XCTAssertEqual("settingsUpdate", task.type)
XCTAssertEqual(Task.Status.succeeded, task.status)
if let details = task.details {
if let typoTolerance = details.typoTolerance {
XCTAssertEqual(newTypoTolerance, typoTolerance)
} else {
XCTFail("typoTolerance should not be nil")
}
} else {
XCTFail("details should exists in details field of task")
}
expectation.fulfill()
case .failure(let error):
dump(error)
XCTFail("Failed to wait for task")
expectation.fulfill()
}
}
case .failure(let error):
dump(error)
XCTFail("Failed updating typo tolerance")
expectation.fulfill()
}
}

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

func testResetTypoTolerance() {
let expectation = XCTestExpectation(description: "Reset settings for typo tolerance")

self.index.resetTypoTolerance { result in
switch result {
case .success(let task):
self.client.waitForTask(task: task) { result in
switch result {
case .success(let task):
XCTAssertEqual("settingsUpdate", task.type)
XCTAssertEqual(Task.Status.succeeded, task.status)
expectation.fulfill()
case .failure(let error):
dump(error)
XCTFail("Failed to wait for task")
expectation.fulfill()
}
}
case .failure(let error):
dump(error)
XCTFail("Failed reseting typo tolerance")
expectation.fulfill()
}
}

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

// MARK: Separator Tokens

func testGetSeparatorTokens() {
Expand Down Expand Up @@ -1260,7 +1368,8 @@ class SettingsTests: XCTestCase {
separatorTokens: [],
nonSeparatorTokens: [],
dictionary: [],
pagination: .init(maxTotalHits: 1000)
pagination: .init(maxTotalHits: 1000),
typoTolerance: defaultTypoToleranceResult
)

let expectation = XCTestExpectation(description: "Update settings")
Expand Down Expand Up @@ -1341,7 +1450,8 @@ class SettingsTests: XCTestCase {
separatorTokens: ["&"],
nonSeparatorTokens: ["#"],
dictionary: ["J.K"],
pagination: .init(maxTotalHits: 500)
pagination: .init(maxTotalHits: 500),
typoTolerance: nil
)

let expectedSettingResult = SettingResult(
Expand All @@ -1356,7 +1466,8 @@ class SettingsTests: XCTestCase {
separatorTokens: ["&"],
nonSeparatorTokens: ["#"],
dictionary: ["J.K"],
pagination: .init(maxTotalHits: 500)
pagination: .init(maxTotalHits: 500),
typoTolerance: defaultTypoToleranceResult
)

self.index.updateSettings(newSettings) { result in
Expand Down
Loading

0 comments on commit 827adb4

Please sign in to comment.