Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Fix custom scout keys not being utilized when deleting from queue #656

Conversation

stevebauman
Copy link
Contributor

@stevebauman stevebauman commented Sep 30, 2022

Closes #655
Closes #653

This PR resolves an issue described in #653 where the MeiliSearchEngine (as well as the AlgoliaEngine, it turns out) does not utilize the custom scout key when removing models from the search index when deleting from the queued RemoveFromSearch job.

The issue, is that when the faux model instances (faux, because they don't actually exist in the database) are instantiated from the RemoveFromSearch job after unserialization, all of the model's attributes are (of course) not available. The models are instead force-filled with the restored from queue custom scout keys:

return tap(new $value->class, function ($model) use ($id) {
$keyName = $this->getUnqualifiedScoutKeyName(
$model->getScoutKeyName()
);
$model->forceFill([$keyName => $id]);
});

This means, that the getScoutKey() method on these faux model instances will not return the appropriate keys inside of the delete() methods of their respective Engine when they are retrieved, as they will be overridden, and all other model attributes will be missing.

Another issue, was that using a custom scout key that returned as a string, would interfere with Eloquent's primary key type casting. If the primary key of the searchable model matches the unqualified scout key (which is likely always the case), then Eloquent would attempt to cast the string as an integer when retrieved from the model naturally. This would result in getScoutKey() returning 0 if the returned custom key is prefixed with a string.

To resolve this, I've implemented a check to determine if the Scout key is a string, and alter the primary key type of the faux model instance to suit, so this issue no longer occurs:

https://github.com/stevebauman/scout/blob/8b9270f812e8de28e6371ecd5ecb20e61f491116/src/Jobs/RemoveFromSearch.php#L59-L68

return tap(new $value->class, function ($model) use ($id) {
    // The scout key may not be an integer. In this case,
    // we will force a key type of string so it is not
    // cast when retrieving it from the model.
    $model->setKeyType(
        is_string($id) ? 'string' : 'int'
    )->forceFill([
        $model->getUnqualifiedScoutKeyName() => $id
    ]);
});

@DarronEngelbrechtEdge This should resolve your issue -- can you review this PR? I've included tests that cover your use case.

Let me know if you'd like anything adjusted, thanks for your time! ❤️

@driesvints
Copy link
Member

Thanks a lot @stevebauman! Great work.

Did you keep in mind the changes that are present on master? Do we need to adjust a lot when merging this into master later on?

I'd love a review by you @mmachatschek if you're willing.

@driesvints driesvints marked this pull request as draft September 30, 2022 14:43
@stevebauman
Copy link
Contributor Author

Happy to help @driesvints! 🙏

Did you keep in mind the changes that are present on master?

I didn't -- let me checkout master and see what the differences are and I'll comment back when I'm done!

@stevebauman
Copy link
Contributor Author

stevebauman commented Sep 30, 2022

Ok I've submitted a draft PR for the master (10.x) branch with this PR's changes 👍

Let me know your thoughts!

@DarronEngelbrechtEdge
Copy link

@stevebauman - Many thanks this PR seems like it will sort all uses cases and both engines. I will have time again on Monday to test it in my project and confirm that it's working for me.

Copy link

@DarronEngelbrechtEdge DarronEngelbrechtEdge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

$searchableData,
$model->scoutMetadata()
$model->scoutMetadata(),
[$model->getKeyName() => $model->getScoutKey()],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevebauman - PS. If people are already adding the key in their toSearchableArray() with some value other than what's returned from getScoutKey(). Worth considering perhaps? Might also then want to make this change in the MeiliSearchEngine to ensure the objectID key is not overridden?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also something that jumped to my eye. The order here was definitly on purpose the way it was before. is there a specific reason why this was change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevebauman can you maybe answer this question?

Copy link
Contributor Author

@stevebauman stevebauman Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply on this one guys! I modified this due to conflicts with the searchable data array. The priority here was intended.

$searchableData will contain the model's attributes, including its primary key, alongside other data:

['id' => 1, '...']

This means, the custom Scout key will always be overridden (unless the model uses a different primary key).

Without this, the array_merge() structure would be:

array_merge(
    ['id' => 'custom-scout-key:1'],
    ['id' => 1],
    ['metadata'],
)

Leading to the custom scout key always being overridden by the primary key.

@DarronEngelbrechtEdge PS. If people are already adding the key in their toSearchableArray() with some value other than what's returned from getScoutKey(). Worth considering perhaps?

I'm not really sure how we can account for that due to the above scenario... The primary key will likely always be present, so the custom Scout key will never be included in the indexed data. Correct me if I'm wrong?

Might also then want to make this change in the MeiliSearchEngine to ensure the objectID key is not overridden?

Don't you specify the primary key with the MeilisearchEngine? I can't find any reference to objectID inside of Meilisearch's documentation.

Copy link
Contributor Author

@stevebauman stevebauman Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementations the above array_merge() priority for the MeiliSearchEngine and AlgoliaEngine differ, as Algolia requires a primary key of objectID. You cannot change it (to my knowledge). While MeiliSearch allows you to change it:

return array_merge(
['objectID' => $model->getScoutKey()],
$searchableData,
$model->scoutMetadata()
);

Since objectID is unlikely to be a models primary key, collisions in the array_merge() are unlikely, so I haven't adjusted it. Maybe we should to ensure the same "priority" consistency across the included engines?

AlgoliaEngine:

$objectIds = collect($results['hits'])->pluck('objectID')->values()->all();

MeiliSearchEngine:

$objectIds = collect($results['hits'])->pluck($model->getKeyName())->values()->all();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure how we can account for that due to the above scenario... The primary key will likely always be present, so the custom Scout key will never be included in the indexed data. Correct me if I'm wrong?

@stevebauman - You're correct, the array_merge() must decide if it wants to override the toSearchableArray() or use the value from there if the user has specified the key returned by getScoutKeyName().

I am in favor of your method change which will override toSearchableArray() as this will ensure the rest of the functionality will operate on the correct ID giving the user some protection against bad configurations? Either way it might be worth a mention somewhere in the docs if it's not there already?

Don't you specify the primary key with the MeilisearchEngine? I can't find any reference to objectID inside of Meilisearch's documentation.

@stevebauman - Sorry noticed a typo in my previous suggestion, I meant to make the change in the AlgoliaEngine to be sure that the user can't override the objectID key in the toSearchableArray() but can only modify the value with getScoutKey() as intended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry noticed a typo in my previous suggestion, I meant to make the change in the AlgoliaEngine to be sure that the user can't override the objectID key in the toSearchableArray() but can only modify the value with getScoutKey() as intended.

No worries! Understood, I agree with this change. I've moved the order of this in the AlgoliaEngine as well. 👍

@DarronEngelbrechtEdge
Copy link

@stevebauman - Thanks again I can confirm this resolves issue #653 in my project (using MeiliSearchEngine).

@stevebauman
Copy link
Contributor Author

That's excellent to hear @DarronEngelbrechtEdge! Thank you for taking the time to review the PR and testing it out ❤️

@DarronEngelbrechtEdge
Copy link

That's excellent to hear @DarronEngelbrechtEdge! Thank you for taking the time to review the PR and testing it out heart

@stevebauman Only a pleasure, thanks to you lot for saving so many people so much time!

@driesvints
Copy link
Member

So where are we with this? Is this ready for review?

@stevebauman stevebauman marked this pull request as ready for review October 4, 2022 12:15
@stevebauman
Copy link
Contributor Author

Ready for review now @driesvints! I've made one last change in regards to @DarronEngelbrechtEdge comment. I've moved the array_merge() priority for the AlgoliaEngine as well for unification across the engines:

3fdb281

return array_merge(
    $searchableData,
    $model->scoutMetadata(),
    ['objectID' => $model->getScoutKey()], // Moved below so it cannot be overridden.
);

@driesvints
Copy link
Member

Thanks @stevebauman this looks good. @mmachatschek does this also look good to you?

Copy link
Contributor

@mmachatschek mmachatschek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@driesvints The changes look good to me after @stevebauman answered my concerns

Copy link
Member

@driesvints driesvints left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks everyone for your work/feedback on this!

@taylorotwell taylorotwell merged commit 19d77a4 into laravel:9.x Oct 4, 2022
@taylorotwell
Copy link
Member

Thanks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MeiliSearchEngine does not work with custom Scout keys
5 participants