How can I make this search query more efficient? - laravel-4

I have a somewhat convoluted search query that I would like to make more efficient (if possible).
Here is the entire code for this query:
Route::post('api/search/{startRow}', function($startRow)
{
$category = Category::where('name', '=', Input::get('category'))->first();
// Initialize query
$query = Resource::with('alerts', 'alerts.type', 'user', 'category', 'comments', 'comments.comments', 'ratings')
->where('duplicate', '=', 0);
// Limit search results
if(Input::get('show'))
{
$show = Input::get('show');
switch ($show) {
case 'verified':
$query->where('verified', '=', true);
break;
case 'unverified':
$query->where('verified', '=', false);
break;
case 'alerted':
$query->has('alerts');
break;
case 'unalerted':
$query->has('alerts', '=', 0);
break;
default:
// The default will be 'all' (show all results)
break;
}
}
if($category->name != "everything")
$query->where('category_id', '=', $category->id);
// Sort the search results
if(Input::get('sort_type'))
{
$sort_by = Input::get('sort_type');
switch ($sort_by)
{
case 'relevance':
break;
case 'name_asc':
$query->orderBy('name', 'asc');
break;
case 'name_desc':
$query->orderBy('name', 'desc');
break;
case 'rating_high':
$query
->leftJoin('ratings', 'ratings.ratable_id', '=', 'resources.id')
->where('ratings.ratable_type', '=', 'Resource')
->orderBy(DB::raw('avg(ratings.score)'), 'desc')
->orderBy(DB::raw('count(ratings.score)'), 'desc')
->select('resources.*')
->groupBy('resources.id');
break;
case 'rating_low':
$query
->leftJoin('ratings', 'ratings.ratable_id', '=', 'resources.id')
->where('ratings.ratable_type', '=', 'Resource')
->orderBy(DB::raw('avg(ratings.score)'), 'asc')
->orderBy(DB::raw('count(ratings.score)'), 'asc')
->select('resources.*')
->groupBy('resources.id');
break;
case 'date_new':
$query->orderBy('created_at', 'desc');
break;
case 'date_old':
$query->orderBy('created_at', 'asc');
break;
default:
break;
}
}
// Search by keyword(s)
if(Input::get('keyword'))
{
$search = Input::get('keyword');
$searchTerms = explode(' ', $search);
$fields = array(
'resources.description',
'resources.website',
'resources.additional_info');
foreach ($searchTerms as $term)
{
$query->where('resources.name', 'LIKE', '%'. $term .'%');
foreach ($fields as $field)
{
$query->orWhere($field, 'LIKE', '%'. $term .'%');
}
}
}
// Search by tag(s)
if(Input::get('tags'))
{
$tags = Input::get('tags');
$query
->select('resources.*')
->join('taggables', 'taggables.taggable_id', '=', 'resources.id')
->join('tags', 'taggables.tag_id', '=', 'tags.id')
->whereIn('tags.id', $tags)
->groupBy('resources.id')
->havingRaw('COUNT(resources.id)=?', array(count($tags)));
}
// Total number of results
$count = $query->get()->count();
// Page number and offset for infinite scroll
$query->skip($startRow)->take(10);
// Get our first set of tiles
$tiles = $query->get();
return Response::json(array(
'count' => $count,
'tiles' => $tiles->toArray()));
});
You see, I have a database filled with "resources" which (through pivot tables) are related to tags, comments and alerts, and I want these resources searchable on any of the following criteria:
Text contained in resource model itself, tags associated with the resource, and number of associated alerts.
One problem I'm having is that the keyword search doesn't seem to be "accurate" enough. When I search for, say, "Venture Firm", there are a few results returned before the one which contains the phrase "Venture Firm" - a user will definitely not expect this.
Another problem I'm having related to selecting a "show" type (i.e. $query->has('alerts') if user only wants to see resources with alerts). If I enter a keyword search and a show type (like above), the results will still contain resources that do not have alerts (even though I specified I only want resources that have alerts).

Relevance search depends on your db engine.
But for the keyword search, you have it wrong:
foreach ($fields as $field)
{
$query->orWhere($field, 'LIKE', '%'. $term .'%');
}
This piece adds WHERE ....long list of clauses here.... OR something LIKE %term% ... what basically breaks the whole thing.
Instead you need this:
$fields = array(
'resources.name',
'resources.description',
'resources.website',
'resources.additional_info'
);
$query->where(function ($q) use ($searchTerms, $fields) {
foreach ($searchTerms as $term)
{
foreach ($fields as $field)
{
$q->orWhere($field, 'LIKE', '%'. $term .'%');
}
}
});
This will wrap your OR .. OR .. clauses in AND ( .. OR .. ).

Related

laravel inside with leftjoin is not working

I have added left join inside with but it is not working at all.
Please suggest or guide.
$q = $q->leftJoin('product_sort_order', function ($join) {
$join->on('products.id', '=', 'product_sort_order.product_id');
$join->on('products.sub_category_id', '=', 'product_sort_order.type_id');
})->orderBy('product_sort_order.sort_order', 'ASC');
Controller code (trying to get category products with relationship)
$category = Category::query();
$category->where('status', '=', '1');
$category->where('id', $sub_category_id);
// $category->where('status', '!=', '2');
$category = $category->with([
'subproducts' => function ($q) use ($request, $sub_category_id) {
$q->select('products.*');
$q->where('products.status', '=', 1);
$q->where('products.is_pendding', '=', 0);
$q->where('products.sub_category_id', $sub_category_id);
if ($request->get('theme')) {
$q->Where('products.theme_id', 'LIKE', '%' . $request->get('theme') . '%');
}
if ($request->get('categories')) {
$q->where('products.category_id', getCategoryIdFromSlug($request->get('categories')));
}
if ($request->get('sections')) {
$q->where('products.section_category_id', getSectionIdFromSlug($request->get('sections')));
}
if ((App::getLocale()) == 'en') {
// $q->orderBy('product_name_lang->en');
} else {
// $q->orderBy('product_name_lang->fr');
}
if ($request->get_j0_product == true) {
$q->where('products.delivery', 'J0');
}
// $q->orderBy('sale_price', 'DESC');
// $q->orderBy('created_at', 'DESC');
// $q = $q->leftJoin('product_sort_order', function ($join) {
// $join->on('products.id', '=', 'product_sort_order.product_id');
// $join->on('products.sub_category_id', '=', 'product_sort_order.type_id');
// })->orderBy('product_sort_order.sort_order', 'ASC');
}
])->first();
model relationship inside category
public function subproducts(){
return $this->hasMany(Product::class,'sub_category_id','id');
}

Removing existing Where clauses

I want to remove where clauses in some conditions:
$q = Thread::with('comments')->take(10);
if(strlen($input) < 4){
$result = $q->where('title', '~', "^$input$")->get();
if($result->isNotEmpty()){
return $result;
}
// if was empty:
}
// How to clean the where clause from the above here? because it affect the query below:
$result = $q->where('title', 'like', "%$input%")->get();
The problem is the first where clause affects the second one if the data was empty, How can i remove existing where clauses when needed ? also the newQuery() is not working in my case.
Note that i'm using two seperate statement in postgres ~ and 'like'
Something like reorder() for where clauses
Yes there is a way to do it
$q = Thread::with('comments')->take(10);
if(strlen($input) < 4){
$result = $q->where('title', '~', "^$input$")->get();
if($result->isNotEmpty()){
return $results;
}
}
// getQuery() is a query builder method that contains all the groupings, selects, orders, wheres, joins etc for the query that you are accessing or trying to build.
$q->getQuery()->wheres= [];
$result = $q->where('title', 'like', "%$input%")->get();
Use Conditional Clauses
$threads = Thread::with('comments')->when(strlen($input) < 4, function ($query) use ($input) {
return $query->where('title', '~', "^$input$");
}, function ($query) use ($input) {
return $query->where('title', 'like', "%$input%");
})->take(10)->get();
https://laravel.com/docs/8.x/queries#conditional-clauses
If you want to give it a second change I would write that logic like this. Comments are loaded only if they are needed.
if (strlen($input) < 4) {
$threads = Thread::where('title', '~', "^$input$")->take(10)->get();
if ($threads->isNotEmpty()) {
return $threads->load('comments');
}
}
return Thread::with('comments')->where('title', 'like', "%$input%")->take(10)->get();
if you cloning before add where
it works the same as deleting where
...
if(strlen($input) < 4){
$result = (clone $q)->where('title', '~', "^$input$")->get();
if($result->isNotEmpty()){
return $results;
}
}
$result = $q->where('title', 'like', "%$input%")->get();
...

Adding closure to select raw queries (sum)

I have many different combinations of select sum from the same table but with slightly different where queries.
foreach ($transactionTypes as $type) {
switch($type) {
case 'Fuel':
$values = \DB::table("transactions")
->select(\DB::raw("user_id, SUM(fuel_charge) as fuel_charges"))
->whereIn('type', ["Charge", "Refund"])
->whereIn('user_id', $userIdsToBeUsed)
->whereDate('created_at', '>=', $fromDate->toDateString())
->whereDate('created_at', '<=', $toDate->toDateString())
->groupBy('user_id')
->get();
break;
case 'Commission':
$userIdsToBeUsed = $userIds->merge($tier3Ids);
$values = \DB::table("transactions")
->select(\DB::raw("user_id, SUM(commission_charge) as commission_charges"))
->whereIn('user_id', $userIdsToBeUsed)
->whereDate('created_at', '>=', $fromDate->toDateString())
->whereDate('created_at', '<=', $toDate->toDateString())
->groupBy('user_id')
->get();
break;
}
}
As you can see, they are slightly different and I have a dozen of cases in the loop; but with this approach, I need to do a dozen queries to the same table.
What I want to ask is, Is there a way to combine them into a single query?
Something like:
$values = \DB::table("transactions")
->select(
[
\DB::raw("user_id, SUM(fuel_charge) as fuel_charges") => function($q) {
// $q->where(...)
}
],
[
\DB::raw("user_id, SUM(commission_charge) as commission_charges") => function($q) {
// $q->where(...);
}
])
->get()
A quick thought. This can be a pretty neat hack you can say:
$userIdsToBeUsed = $userIds->merge($tier3Ids);
foreach ($transactionTypes as $type) {
$values = \DB::table("transactions")
->select(\DB::raw("user_id, SUM(" . strtolower($type) . "_charge) as " . strtolower($type) . "_charges"))
->whereIn('type', ["Charge", "Refund"])
->whereIn('user_id', $userIdsToBeUsed)
->whereDate('created_at', '>=', $fromDate->toDateString())
->whereDate('created_at', '<=', $toDate->toDateString())
->groupBy('user_id')
->get();
}
I just use the lowercase of $type in query

How to add and exclude where() clause in laravel query builder

How to add and exclude where() clause in laravel query builder so that in one case it will be added in other one no
$orders = DB::table('orders')->select(
'orders.id as id',
'orders.created_at as created',
'u.email as email',
'ud.id as data_id',
'ud.firstName as firstName',
'ud.lastName as lastName',
'ud.phone as phone',
's.name as service',
's.id as service_id',
'pt.id as payment_type_id',
'pt.name as payment_name')
->join('users as u', 'orders.user_id', '=', 'u.id')
->join('user_data as ud', 'u.id', '=' ,'ud.user_id')
->join('payment_types as pt', 'orders.payment_type_id', '=', 'pt.id')
->join('services as s', 'orders.service_id', '=', 's.id')
->where('u.id', $user->id)->orderBy($sortBy, $type)->get();
I want to do this
$order = DB::table()....
if(true){
$order->where('id', '=', 1);
}
$order->orderBy('fieldname', 'asc')->get();
But the example above return no results
For conditional clauses you can make use of when().
$order = DB::table()
->yourQuery(...)
->when($var, function ($query, $var) { // <----
return $query->where('id', '=', 1); // <----
} // <----
->orderBy('fieldname', 'asc')
->get();
You can read more about this in the docs:
Conditional Queries
Sometimes you may want clauses to apply to a query only when something
else is true. For instance you may only want to apply a where
statement if a given input value is present on the incoming request.
You may accomplish this using the when method:
$role = $request->input('role');
$users = DB::table('users')
->when($role, function ($query, $role) {
return $query->where('role_id', $role);
})
->get();
The when method only executes the given Closure when the first
parameter is true. If the first parameter is false, the Closure will
not be executed.
...

return all the posts where author.username equals $username

I have a posts model that has this relationship to the User:
public function author()
{
return $this->belongsTo(User::class, 'user_id');
}
i am building a search function and I have this code
public function index(Request $request)
{
$search = $request->search;
$filter = $request->filter;
$append = array();
$posts = Post::with('author')->with('categories')->latest();
if($search){
switch ($filter) {
case 'username':
$posts->author->where('username', 'LIKE', '%'. $search . '%');
break;
}
$append += array('search' => $search);
$append += array('filter' => $filter);
}
$posts = $posts->paginate(3);
$posts->appends($append);
return view('core.blog.posts.index', compact('posts'));
}
I get
Undefined property: Illuminate\Database\Eloquent\Builder::$author
How do I add where that looks for author based on his username? I must be able to add this condition in an if case
You want to use whereHas() to searched based on a relation. This will return only posts that have an author with the username.
switch ($filter) {
case 'username':
$posts->whereHas('author', function($q) use($search) {
$q->where('username', 'LIKE', '%'. $search . '%');
});
break;
}
UPDATE
If you need conditional where, you can do this:
$postSelector = Post::with('categories');
if($search){
switch ($filter) {
case 'username':
$postSelector->with(['author' => function($q) use($search) {
$q->where('username', 'LIKE', '%'. $search . '%');
}]);
break;
}
$posts = $postSelector->get();

Resources