Laravel - Full Text Search on Multiple Tables - php

I am using Laravel 4 and the Eloquent model for a project we are working on.
The database conforms to 3NF and everything works great. Also all MySQL tables were switched back from InnoDB to MyISAM since the MySQL version < 5.6 (full text search in InnoDB is only supported from 5.6 and up).
While creating some database search filters I am finding some shortage with using the Eloquent model vs the Query Builder. In specifics, especially when trying to do a full text search on columns from multiple tables (and staying within the Eloquent's object context).
For simplicity, we have the following database structure:
--projects
--id
--name
--status
--...
--users
--id
--...
--roles
--id
--project_id
--user_id
--...
--notes
--id
--project_id
--user_id
--note
--....
The following code (simplified and minimized for the question) currently works fine, but the full text search only works for one table (projects table).
if (Request::isMethod('post'))
{
$filters = array('type_id','status','division','date_of_activation','date_of_closure');
foreach ($filters as $filter) {
$value = Input::get($filter);
if (!empty($value) && $value != -1) {//-1 is the value of 'ALL' option
$projects->where($filter,'=',$value);
}
}
$search = Input::get('search');
if (!empty($search)) {
$projects->whereRAW("MATCH(name,description) AGAINST(? IN BOOLEAN MODE)",array($search));
}
}
// more code here...
// some more filters...
// and at the end I am committing the search by using paginate(10)
return View::make('pages/projects/listView',
array(
"projects" => $projects->paginate(10)
)
);
I need to extend the full text search to include the following columns - projects.name,projects.description and notes.note.
When trying to find how to make it with Eloquent we keep on coming back to Query Builder and running a custom query, which will work fine but then we will face these problems/cons:
Query Builder returns an array while Eloquent returns model objects. Since we are extending each model to include methods, we really don't want to give up the awesomeness of the Eloquent model. And we really don't want to use the Eloquent Project::find($id) on the return results just to get the object again.
We are chaining the 'where' methods to have any number of filters
assigned to it as well as for code re-usability. Seems like mixing
Eloquent and Query Builder statement together will break our chaining.
For the consistency of this project, we want all database queries to
stay in Eloquent connotation.
Reading Laravel's documentation and API, I could not find a method to run raw SQL queries using Eloquent. There is whereRAW() but it is not broad enough. I assume that this is a restriction made by design, but it is still a restriction.
So my questions are:
Is it possible to run a full text search on columns from multiple tables, only in Eloquent. Every piece of information I came across online, mentions using Query Builder.
If not, is it possible to use Query Builder searches and returning Eloquent objects? (without the need to run Project::find($id) on the array results).
And lastly, is it possible to chain Eloquent and Query Builder where methods together, while only committing using get() or paginate(10) at a later point.
I understand that Eloquent and Query Builder are different creatures. But if mixing both was possible, or using Eloquent to run raw SQL queries, I believe that the Eloquent model will become much more robust. Using only Query Builder seems to me a bit like under-using the Laravel framework.
Hope to get some insights about this since it seems as the forums/community of Laravel is still evolving, even though I find it to be an amazing framework!
Thanks and I appreciate any input you may have :)

First of all you can use query scope in your model/s
public function scopeSearch($query, $q)
{
$match = "MATCH(`name`, `description`) AGAINST (?)";
return $query->whereRaw($match, array($q))
->orderByRaw($match.' DESC', array($q));
}
this way you can get "eloquent collection" as return
$projects = Project::search(Input::get('search'))->get();
then, to search also into notes you can make a more complex scope that join notes and search there.

Not sure if this will help but there is a workaround in innoDB(in versions that support fulltext search), maybe it works for you.
Lets use 'notes' as second table
SELECT MATCH(name,description) AGAINST(? IN BOOLEAN MODE),
MATCH(notes.note) AGAINST(? IN BOOLEAN MODE)
FROM ...
.
WHERE
MATCH(name,description) AGAINST(?) OR
MATCH(notes.note) AGAINST(?)

Related

How to perform a query having pseudo-relations between two distinct database types?

In a nutshell, the title best discribes my question, but here I am showing the core of the problem.
I have two databases in my web application, One is MariaDB, the other is MongoDB, To give some context, the "user" table in MariaDB stores user information with column "id" it's primary key, there is another "badge" table which stores badge information with also column "id" it's primary key, at last there is "user_badge" collection in MongoDB having documents of fields
{_id, user_id, badge_id, date}
which just links the User with his/her Badges. This is what I meant by pseudo-relation, Unfortunately I don't know what is it called in this situation.
An example:
I want to query and get all users that have a badge with ID 1. So my pseudo-query should do something like "Select all fields from user table where badge_id in user_badge collection is 1". I highlighted like here because this is impossible to be done in a query (based on my knowledge) somehow a query ought to be made on the MongoDB database first then a second have to be made in the MariaDB database against the results of the former query.
Edit: My original question was about how to implement this in Yii2 PHP framework, but when I googled for sometime and I found out no information to do such a thing even in pure PHP, So I decide to end my edited question here, asking for a way to query between a table in an sql database and a collection in a no-sql database, Yet below I leave my old question which just asks for how to do this more specifically in the PHP framework. really if I knew how to do this in pure PHP I can just make a function somehow that does that in the framework if there wasn't any.
Obviously there cannot be a direct primarykey-foriegnkey relation between two database types but I overrided this issue by having a ::hasMany ActiveRecord method in my User Model, and that worked perfectly fine; When I have a User model between hands I just call $model->userBadges to get from MongoDB all documents having that User ID, also vice versa. The problem is when I do a Query involving this relation, I get error
Calling unknown method: yii\mongodb\ActiveQuery::getTableNameAndAlias()
Parts of my Application
User getUserBadges method in User model
public function getUserBadges(){
return $this->hasMany(UserBadge::className(), ['user_id' => 'id']);
}
UserBadge model extending yii\mongodb\ActiveRecord
class UserBadge extends ActiveRecord
{
public static function collectionName()
{
return 'user_badge';
}
public function attributes()
{
return ['_id', 'user_id', 'badge_id', 'date'];
}
public function getUser(){
return $this->hasOne(User::className(), ['id' => 'user_id']);
}
public function getBadge(){
return $this->hasOne(Badge::className(), ['id' => 'badge_id']);
}
}
My query
$query = User::find()->joinWith(['userBadges']);
Edit: I figured out that the previous query is not really what I want, I simplified it to be clear but the real query that I want to do and you will get the point of why I am doing all of this is
$query = User::find()->joinWith(['userBadges'])->where(['badge_id' => 1]);
And with that I can get users from the user table who have a certain badge with id for example 1.
And here the code fails and throws the error stated above. After inspecting for sometime I found the API for the joinWith method
This method allows you to reuse existing relation definitions to perform JOIN queries. Based on the definition of the specified relation(s), the method will append one or multiple JOIN statements to the current query.
And here I knew that it's normal for this error to occur, In my query I am joining a document in a collection of the MongoDB database not a record in a table in a SQL database which definitely wouldn't work. I got stuck here and don't know what to exactly do, I am sticking to have user table in a SQL database and having the user_badge collection in a no-SQL database, what shall I do in such scenario? query on the no-SQL first and then query a SQL query against the result of the former query? or there is already a solution to such a problem in the methods of AcitveQuery? Or my Database structure is invalid?
Thanks in advance.
So after some good time I knew how to do it with the help of this question, where a SQL query is made against a PHP array.
So, first MongoDB will be queried and the results will be stored in an array, then A MariaDB SQL query will be made against the array generated from former query, I am pretty sure that this is not the best option; what if the result of the MongoDB query 100,000? well an array will be made with 100,000 entries, the SQL query will be made using also that 100,000 item array. Yet this is the best answer I could get (until now).
How to implement it in Yii2
// This line query from the MongoDB database and format the data returned well
$userBadges = UserBadge::find()->select(['user_id'])->where(['badge_id' => 1])->column();
// This line make the SQL query using the array generated from the former line
$userQuery = User::find()->where(['id' => $userBadges]);
I hope there can be a better answer for this question that someone can know, But I thought of sharing what I have reached so far.

question about laravel relationships and performance

i hope you are having a good time. i am learning laravel and the inscuctor talked about when you load relationships in laravel, like so
public function timeline()
{
$ids = $this->follows()->pluck('id');
$ids->push($this->id);
return Tweet::whereIn('user_id', $ids)->latest()->get();
}
and i have a follows relationship in my model, and he talked about this line
$ids = $this->follows()->pluck('id');
being better for performance than this line
$ids = $this->follows->pluck('id');
my question is, how does laravel pluck the ids in the first case, and how it queries the database
i hope im making sense, thanks for your time, and answer.
the following one executes a select query on database
$this->follows()->pluck('id');
the follows() returns a query builder (which is a not yet executed sql statement) and then on the result select the id column and returns a collection of ids
you can see the query by dumping the query builder by $this->follows()->dd()
Whereas in the second option
$this->follows->pluck('id')
up until $this->follows laravel executes a query and returns all the records as a collection instance, You will be able to see all the attributes on each of the records. And then ->pluck('id') is getting executed on the laravel collection class, which will do an operation I assume similar to the array_column function does and returns only the id column.
as you can easily see in the second operation the whole data set was retrieved first from the DB and then selected the required attribute/column (2 distinct and heavy operations). Where as in the first option we directly told eloquent to select only the required column, which is only one lighter operation compared to the second option.

Show countif mysql query table to html

I have a countif query in mysql and I would like to show the table on my html. I'm currently using laravel 6.0 framework.
Here is the picture of the table i want to show:
Here is my code in html:
Here is my code in the controller:
There should be numerous errors with index function in your controller. Specifically with how you are trying to assign $count a value. Read these: Eloquent Methods all() vs get(), Eloquent Selects, Eloquent Ordering, Grouping, Limit and Offset, Eloquent Where.
Laravel has an excellent documentation, if you were to follow it - working with Laravel would become much easier.

Optimising Laravel query

Picked up Laravel recently for an API project and have got it all set up and working, and Ive been migrating an API written from a php function with a concatenated SQL string into Laravel query builder, all is fine apart from one part.
I have a table of stock, and a features table, so I have defined a hasMany relationship on the model of stock to the features, and when I pass in a string array of features to the query I need to loop over each feature and then get back all the stock items that have those features.
Using a whereIn is not what I need as that will give me stock that has only one of the features. If I use a standard where that will give me 0 results as soon as multiple features are added.
I have achieved what I want with the following code, but its terribly slow to execute but I am unsure of a better way to get the same result:
foreach($value as $v){
$builder->whereHas('features', function($query) use ($v) {
$query->where('code', $v);
);
}
Use query builder instead of Eloquent to optimize queries...
For example:
DB::table('stock')->join('features',function($join){
$join->on('features.id','=','stock.feature_id')
->where('code', $v);
})
->select('') <--- also select columns here..
->get();
Docs: https://laravel.com/docs/5.7/queries

How do I efficiently limit query to the presence of an id in a many-to-many relationship in laravel?

The scenario:
I am building a search system for a given set of data. Most of this is straight forward - where record title contains X, where record date before Y, etc. Where I am running into difficulty is essentially a category search. Each record belongs to zero or more categories (a relationship pivot table exists such that each row contains a record and a category), and when the user searches for Category A, I want to return all of the records that belong to that category. I've gotten this working with a whereHas, but it seems inordinately slow. In this instance, assume $category is a numeric id that is correctly validated as a category, and $records is an eloquent query builder that has not yet been executed by a get, pagination, all, etc (my function checks to see if several $request->input parameters are defined, then attaches a where to $record as required by the specified parameters, only executing it after all parameters have been considered):
if(!empty($category)) {
$records = $records->whereHas('categories', function($query) use ($category)
{
$query->where('category_id', $category);
});
}
This works, but as there are 7000+ records, 7000+ relationships defined in the pivot, and roughly 30 'categories', the search takes longer than I am comfortable leaving it. My unconfirmed thought is that the where query is executing for every record, thus leading to hundreds or thousands of queries.
I've debated approaching this using the raw query builder and just passing the list of record id's that have that category and using a simple where to filter the record collection before it's executed, but it seems counter-intuitive, leading me to believe there must be a better way.
The Question
How do I efficiently limit the records returned by $records->get() to those records with a defined relationship to category $category.
Edit 2018-01-16
To clarify, while I could simply do $category->records to return all records belonging to a category, this is part of a larger search engine. The full structure of the code looks like this:
If($subject_search_term) {
$records->where('subject', $subject_search_term)
}
If(some other search criteria is defined) {
$records->where(someothercriteria);
}
If(Category search criteria is defined) {
$records->whereHas(something);
}
$records->paginate(20);
Furthermore, there are two of these many-to-many relationships that I need to query (in addition to 'categories', lets say there is also a 'subject' that is independent of it, but similar structure and idea). As far as I know, I need to build the query off records and filter it accordingly.
EDIT 2
For anyone else with this problem, it seems the vastly more efficient way (and the most efficient that I've found) is Joel Hinz's comment - use the DB facade to build a raw query, pluck the id's from it, and use that in a whereIn clause.
Try this:
mpyw/eloquent-has-by-non-dependent-subquery: Convert has() and whereHas() constraints to non-dependent subqueries.
mpyw/eloquent-has-by-join: Convert has() and whereHas() constraints to join() ones for single-result relations.
if(!empty($category)) {
$records = $records->hasByNonDependentSubquery('categories', function($query) use ($category)
{
$query->where('category_id', $category);
});
}
That's all. Happy Eloquent Life!

Categories