Laravel Scout - Where not equal to - laravel

I'm building a query for Laravel Scout.
$subchanResults = Subchan::search($query)
->when($request->input('incl_nsfw'), function ($query, $incl_nsfw) {
if ($incl_nsfw == 0) {
return $query->where('nsfw','!=', 'always');
}
}, function ($query) {
return $query->where('nsfw','!=', 'always');
})
->paginate(3);
Laravel Scout only allows basic where clauses, no advanced conditional clauses of any kind, so it's been kind of tricky so far. (Documentation: https://laravel.com/docs/8.x/scout#where-clauses)
My problem is with this line: return $query->where('nsfw','!=', 'always');
It seems that Scout will allow a simple where('column','value') , but if I try to add the 3rd parameter to the where clauses (not equal), it doesn't work.
How can I make this query work? Or do I have to manually trim the results myself after the query?

The docs you linked to explain:
Scout allows you to add simple "where" clauses to your search queries. Currently, these clauses only support basic numeric equality checks and are primarily useful for scoping search queries by an owner ID. Since a search index is not a relational database, more advanced "where" clauses are not currently supported
Basically, when you call ::search you are no longer building an Eloquent ORM query; you’re building a Scout query, and the where method for a Scout query only accepts two parameters: a key and a value to match against:
https://github.com/laravel/scout/blob/7a8d5b90761e0c102d357dd9e57d8c2f726ceaa5/src/Builder.php#L103-L110

I was hoping to find a workaround within Scout itself, but it seems that conditional clauses are just completely out of the question at the moment for scout. Here is my solution for filtering at the search engine level (Meilisearch) for the same scenario:
$subchanResults = Subchan::search($query, function ($meilisearch, $query, $options) use ($request) {
if ($incl_nsfw = $request->input('incl_nsfw')) {
$options['filters'] = 'nsfw != always';
}
return $meilisearch->search($query, $options);
})->get();

Related

Laravel Scout/Meilisearch - filter by a non-searchable column

I want to make it so that I can filter results based on a column that isn't searchable with Meilisearch and Laravel scout.
So imagine a "Comment" table, with the following searchable columns:
public function toSearchableArray() {
$array = Arr::only(
$this->toArray(),
['id','title', 'link', 'raw_text', 'subchan', 'nsfw']
);
return $array;
}
But only get results past a certain date:
Comment::search($query, ['filters' => 'created_at > 795484800'])
To do this, I need to add created_at scout's toSearchableArray. The problem with this is that when a user searches, results from created_at will also be queried.
If I understand you correctly you want to be able to filter based on the created_at column, but it shouldn't be searchable, ie entering "795" as a query shouldn't return all results where "795" is part of the timestamp?
I don't think Scout will allow you to achieve this in a simple way at the moment but it should still be possible.
Step 1 is to add the created_at column to the toSearchableArray() method. This will ensure the data is indexed by Meili.
Step 2 is to alter the configuration of the index where your model is searchable in order to exclude created_at from the list of searchable attributes. This is psuedo code and undocumented but it should look something like this:
$dummy = new Comment();
// Should resolve to an engine: https://github.com/laravel/scout/blob/f8aa3c3182fe97f56a6436fd0b28fcacfcbabc11/src/Searchable.php#L279
$engine = $dummy->searchableUsing();
// Should resolve to MeiliSearch\Endpoints\Indexes via a magic method that resolves the underlying Meili driver:
// https://github.com/laravel/scout/blob/33bbff0e3bfb1abd0ea34236c331fc17cdeac0bc/src/Engines/MeiliSearchEngine.php#L298
// ->
// https://github.com/meilisearch/meilisearch-php/blob/f25ee49b658f407af3d3f1f9a402997e7974b6bb/src/Delegates/HandlesIndex.php#L23
$index = $engine->index($dummy->searchableAs());
// https://github.com/meilisearch/meilisearch-php/blob/f25ee49b658f407af3d3f1f9a402997e7974b6bb/src/Endpoints/Delegates/HandlesSettings.php#L55
$index->updateSearchableAttributes(
['id','title', 'link', 'raw_text', 'subchan', 'nsfw']
);
Once created_at is indexed but not searchable you want to filter on the value. Meili has operators for numeric values.
Step 3 is to do a custom search using Scout:
Comment::search($query, function (Indexes $meilisearch, $query, $options) {
$options['filters'] = 'created_at>795484800';
return $meilisearch->search($query, $options);
});
Again, this is pseudo code – I haven't tested any part of it. I would really appreciate if Scout would implement support for customizing the index' settings on creation or exposing a method for updating the settings, allowing you to add driver specific settings in your configuration file for example.
i spent numerous hours debugging and getting the filter for dates to work.
this wont work as the where clause only accepts two arguments
Comment::search($query)->where('created_at', '>', 795484800)->get();
this also wont work because the arguments passed are not part of the two options that they have supported in the scout library
Comment::search($query, function (Indexes $meilisearch, $query, $options) {
$options['filters'] = 'created_at>795484800';
return $meilisearch->search($query, $options);
});
my solution for everyone out there trying to get this to work is to use the following:
$results = Event::search(
query: $request->get('query'),
callback: function (Indexes $meilisearch, $query, array $options) use ($request, $from, $to) {
$options['filter'] = "from <= 1667692800";
// dd($options);
return $meilisearch->rawSearch(
$query,
$options,
);
},
)->paginate();
hope this helps anyone else having issues as this wasted my morning searching for solutions until i decided to dig into the code in the library.
I solved my problem by using filterable attributes of Meilisearch. But it needs to be configured before running the search. I used php artisan tinker to solve this as follows, you might want to write an artisan command to do so.
$client = new MeiliSearch\Client('https://url_to_meilisearch_instance:7700');
$client->index('comments_index')->updateFilterableAttributes(['created_at']); // Replace your index_name
And that's about it. If you have a rather large dataset, you might want to run the following command to check the status:
$client->index('comments_index')->stats();
If the response contains isIndexing => false you're good to go. Now you may run the filter as usual,
Comment::search($query)->where('created_at', '>', 795484800)->get();

Too few arguments Laravel Nova relatable query

I have a relationship that needs to be filtered differently depending on the Laravel Nova field it is being used to populate.
A single Process_type has one or more Process_justifications attached as options (BelongsToMany)
A single Process_type also has a single Process_justification set as a default (BelongsTo)
I'm using a relatable query to filter the available options for each of these two fields.
The two fields in my Process_type resource are as follows:
BelongsTo::make('Default', 'process_justification', Process_justification::class)
BelongsToMany::make('Options', 'process_justifications', Process_justification::class)
The relatable query looks like this:
public static function relatableProcess_justifications(NovaRequest $request, $query, Field $field)
{
if ($field instanceof BelongsTo) {
//query builder to filter options
...........
} else {
//return all options
return $query;
}
}
This pattern came from Laravel Nova documentation on Dynamic Relatable Models (https://nova.laravel.com/docs/3.0/resources/authorization.html#dynamic-relatable-methods)
It describes adding the field as a third parameter to the relatable query, then using it in a conditional to choose filter logic.
The error I'm getting is
Too few arguments to function App\Nova\Process_type::relatableProcess_justifications(), 2 passed and exactly 3 expected
My make() calls have three parameters as per the documentation, any idea what I'm missing?
I'm on Laravel 7.x and Nova 3.x

How to know what columns are presents in a Eloquent query before to execute it in Laravel 5.5?

Im using Laravel 5.5 and I have and QueryBuilder object (from the "Illuminate/Database/Eloquent/Builder" class).
I want to set an orderBy sentence into my query, but only if this field is present and exists in the QueryBuilder object (as column in the select section sentence).
For example, there is an User model, with the following fields ['id', 'firtsname', 'lastname', 'username','description'].
This is my object:
Use App\User;
$query = User::query();
if ($request->input('sort') != null) {
$model_query->orderBy($request->input('sort'), 'ASC');
}
$users = $query->get();
When I execute it, works fine (if I send you consistent data, of course). But if I set a column what does not exists, it sends and exception. So, the question is, how I can get the columns to retrieve from my $query object? To validate it, and if it's presents, execute the ordening code.
To answer your question, you can get the presence status of a column using Schema::hasColumn()
if (Schema::hasColumn('users', $request->sort)) {
//
}
GOING FURTHER
Now this doesn't seem very efficient, and maybe potentially leak data. Better validating your sort input and accept only proper column names:
$request->validate(['sort' => 'in:column1,column2']);

Use Laravel 5.3 Query Builder to replicate Eloquent ORM data structure for Sub Query

I am trying to replicate the result set that I get when using Eloquent ORM, except with Laravel Query Builder. Basically using this code I can get the packs to appear nested within the products so that when I loop them on the view I can further loop the packs within each products. Seems pretty basic right (see result set below).
$get_posts_for_product = Product::where('active', 1)
->with('packs')
->get()->toArray();
I have tried a few ways using Query Builder to get this to work but it joins the packs inline as I thought it would.
What is the best way to get this same Array structure using Query Builder, I am aware that the result set is a different type of array and that is fine but for my project it must be done using Query Builder at this point.
Thanks.
I would say, that is why you have Eloquent: you don't have to worry about how to have those relationships together.
However incase you really want to achieve the same result I will demo this using two tables users and messages:
1st method:
Retrieve the users and transform it by querying the database for relationships:
$result = DB::table('users')->get()->transform(function ($user){
$user->messages = DB::table('messages')->where('user_id', $user->id)->get();
return $user;
});
Downside: Having many users means a lot of db query on messages table.
Upside: less codes to write
2nd method:
Retrieve both tables using all the ids of user to query the messages:
$users = DB::table('users')->get();
$messages = DB::table('messages')->whereIn('user_id', $users->pluck('id')->toArray())->get();
$result = $users->transform(function ($user) use ($messages){
$user->messages = $messages->where('user_id', $user->id)->values();
return $user;
});
Downside: The need to still transform it.
Upside: Less database trips. i.e two queries only.
3rd method
Looks like the second except that you can group messages by 'user_id' then you do no extra filter when transforming users result:
$user = DB::table('users')->get();
$messages = DB::table('messages')->whereIn('user_id', $user->pluck('id')->toArray())
->get()
->groupBy('user_id');
$result = $user->transform(function ($user) use ($messages){
$user->messages = $messages[$user->id];
return $user;
});
Downside: Same with two.
Upside: no extra filter when transforming users.
Other method
Join on both users and messages when querying then transform the response, or simply use it as it is.
PS: Eloquent uses query builder.
The answer is open for update.

laravel query builder, optional search parameters

Laravel newbie looking for guidance. Can anyone please show me the best way to add optional posted search parameters to the following Eloquent query and return all related data and paginated? The optional search parameters are the foreign keys; country_id, season_id and parameter_id.
$dataitems = Dataitem::orderBy('name')->with('country')->with('season')->with('parameter')->paginate(10);
I am reading the docs and have seen the fluent query builder I'm just struggling to recreate this nicely paginated query.
Many thanks.
I am not sure if I understand what you want. But i would do something like this :
Create your base eloquent query :
$query = Dataitem::orderBy('name');
If you have a specific parameter, add the condition to you existant query.
You can repeat this if block for all your optional parameters.
if (Input::has('country_id'))
{
$query->whereHas('country', function($q){
$q->where('id', '=' ,Input::get('country_id'));
});
}
When you're done, paginate:
$query->paginate(10);

Resources