meilisearch / meilisearch-swift

Swift client for the Meilisearch API
https://www.meilisearch.com
MIT License
93 stars 26 forks source link

Add support for search ranking score #413

Closed Sherlouk closed 1 year ago

Sherlouk commented 1 year ago

Pull Request

Related issue

Fixes #398

What does this PR do?

⚠️ This is a breaking change.

I'd love to hear some thoughts from others on how we may be able to avoid this kind of breaking change, but with #398 and #399 new values are being added to the "hit" object. The hit object is currently a generic provided by the user of the library, and as such it's impossible for us to add new attributes to that.

In this PR I've tried to get around this by creating our own encapsulation called "SearchHit" which itself holds a reference to the actual result but also any additional attributes that Meilisearch itself returns. In order to minimise the impact on users of the library I have implemented a "dynamic member lookup" with a keypath, this continues to support (in a completely type safe and compiler safe way) dot notation to variables within the hit result. Which means hits[0].title would still return the title on the hit, even though architecturally it's address is hits[0].result.title. While this does work for the vast majority of use cases, we are unable to support the case where a user tries to cast a hit to a type, you can see this in our tests where you can no longer cast hits[0] to a "Book", rather it is now a "SearchHit", but again... you can access variables the same and no other changes are needed to the code.

Importantly though this workaround with dynamic member lookups is limited to the capabilities of keypaths, which as documented officially by Apple here does not include methods. As such where users have functions within their models, they would have to call that through hits[0].result.someFunction().

I do personally think this is the best compromise. It is a breaking change, but most will not be impacted in any meaningful way. If others have a better idea though I'd love to hear it. This PR lays up the foundations for #399 which would be significantly easier once this is merged. But let's discuss.

PR checklist

Please check if your PR fulfills the following requirements:

Thank you so much for contributing to Meilisearch!

curquiza commented 1 year ago

@Sherlouk thank for your PR Multiple points

Sherlouk commented 1 year ago

Rebase completed, tests updated, and all running locally.

Breaking Change

This is a breaking change affecting the results returned by use of the search(_:) function.

Previously, you would get back a response of Searchable<T> where "T" is a generic type representing the model of the document stored in MeiliSearch. Searchable includes an array called "hits" - the search results: an array of documents matching the query parameters.

With this PR, instead of "hits" being an array of documents, it is now an array of SearchHit<T>'s. You must now access the document contents by using hits[0].document instead of simply hits[0]. This encapsulating type allows us to add metadata unique to the hit but that is not part of the document itself (including the ranking score, ranking stats, match positions, formatted data, and any future search API expansion - without more breaking changes).

For convenience, this PR includes a dynamic member lookup. This isn't strictly necessary, but it does reduce (and in many cases completely avoids) any errors caused by this PR. It has some limitations, which I'll explain with some code snippets:

let result: Searchable<Book> = ... // unchanged

// accessing members (variables) on the document continues to work without change, thanks to the dynamic member lookup
print(result.hits[0].title) // VALID

// ---

// the result hit is no longer an instance of "T"
let book: Book = result.hits[0] // NEW ERROR
print(book.title)

let book: SearchHit<Book> = result.hits[0] // FIXED
print(book.title)

let book = result.hits[0] // ALSO FIXES ERROR
print(book.title)

let book: Book = result.hits[0].document // ALSO FIXES ERROR
print(book.title)

// ---

// functions do not work with dynamic member lookup
result.hits[0].getStock() // NEW ERROR
result.hits[0].document.getStock() // WITH FIX

Worst case, all errors caused by this PR simply need the developer to add .document to their usage as seen with the last example above. Best case, it continues to work with no changes necessary thanks to the dynamic member lookup.

curquiza commented 1 year ago

Thank you @Sherlouk Following this breaking, do we need to update some examples the README or in the code samples file like this one (getting_started_add_documents_md)?

Sherlouk commented 1 year ago

No documentation currently references the code which is changed by this PR, so no updates needed.

Sherlouk commented 1 year ago

It's worth noting (though not necessarily required for this PR) that I do plan on doing a full review of code samples and documentation to improve adoption of the project. There are others that are outdated (but none because of this PR that I can see).

curquiza commented 1 year ago

It's worth noting (though not necessarily required for this PR) that I do plan on doing a full review of code samples and documentation to improve adoption of the project. There are others that are outdated (but none because of this PR that I can see).

Good to know! I will open an issue regarding it: I have scripts to know which code samples are missing and which ones should be removed 😊

meili-bors[bot] commented 1 year ago

Build succeeded: