Site-wide Search with Laravel Scout

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] hey there sam having a search everywhere box in our app is certainly handy but implementing a site-wide search function in a laravel app may not be as straightforward as you think laravel scout is great however it only allows us to search against one single model and if you want to implement the site-wide search functionality which involves searching multiple models at the same time we definitely need more juice for that so here i created a very basic app which only has three models users post and comments and user can have many posts and comments and the post can have many comments now let's dive into the code so here i have a fresh larval installation and i've installed scout and i'm using the tnt search scout driver instead of algolia tnt search is free and open source and it is a great alternative to algolia the link is in the description if you're interested i've also created a comment and post model using php artisan make command let's quickly set them up so they can work with scout we will implement the searchable trait to enable scout to search on this model we also need to implement the two searchable array method to tell scout which fields in the model that we want to include in a full text search this function should return an array of values which will then convert into the search index by scout let's declare a class constant here to define all the fields that we want to include in the search index the model id is mandatory to include inside the search index for scout to identify individual records if you don't include it scout will complain about it and now back in the two searchable array function we can use the only function to pull out these few values from the model and that's all we need to do in order to set up model for scout let's do the same for our comment model as well alright next we'll create a controller to handle the site-wide search api request we'll run php artisan make controller and in the controller we'll define a method called search and let's connect this method to our api we'll go to api.php and add a new get route which is called site search and the callback function will be our search method we define in our controller and now let's go back to our search method and we'll start coding the search function before we start doing anything let's first write down our story first of all we need to load all the models in our models directory not all the models is using a searchable trade so we should filter them out next for each model we'll call the search function to allow scout to perform the full text search against the model we'll use the search keyword supply in the http request query once we get the results there's a few things that we should include for each result to improve the search experience the first thing is our match which includes the exact matching text and also its surrounding text this will let the user to understand the context of the result better you can think of it sort of like the descriptions in the results when you do a google search next we should also include the model name to let the user know which model does the result belongs to and lastly we also need the view link which is the url that brings the user to the resource so the user can click on it and navigate to that page straight away once we have done that the last step will be combining all these results together and send them back as a http response so that's our plan let's start coding right away first we need to grab the search keyword from the request we assume the user will use the search query parameter when they are performing this get request we'll store it as a variable for now and we'll use it later next we need to get all the models what we're after here are the names of the model files inside our models directory as the model name and file names are the same a way to do this is to load all the file path as an array and we'll map all the path to pass the file name we can use the file facade and call the old file method load all the files inside a directory as an array of spl file info objects now we want to map through each file to extract the file name the spl file info class actually offers a very nice interface for us to get the file name we just need to call the getrelativepath name function on it and just to test if it is working we'll dial and down the file name and we'll quickly serve our webpage just run php artisan serve in our console and in the browser we see comment.php which is the first file in the models directory that's good but we don't really want the php file extension we can quickly remove it by calling the substring function by removing the last four characters on the string but we have another issue here we can't just assume that every files in this folder are php files there could be some other files living inside this folder let's do a check to make sure that we're only getting php files we'll use the substring function again to get the last four characters of the file and if they are not equal to dot php will return now that should do for now let's move on to the next step we only want the models that implement the searchable trait let's filter the result of our map the filter callback function will take in a class name which is a nullable string the first thing we want to do in here is to check whether this string is now or not and remember it's only now when it's not a php file we'll return false to exclude it from the filter result now we want to check if the class here is actually an eloquent model in other words we need to find a way to obtain more information about this class a good way to do this is to use php reflection class the reflection class allows us to get information on any class that we fit to its constructor and it must be a fully qualified class name the add get namespace function here allows us to get our primary app namespace in our case here which is just a word app once we got the reflection instance we can easily check whether the class is an instance of eloquent model by calling the is subclass of function and for the argument we're just passing the eloquent model class name and to find out whether the model is searchable or not we just need to check if the model has a search method in it or not the reflection can do that as well we just need to call the has method function on it and pass in the method name in this case will be search and the filter condition will be if the class is a model and it is searchable and that will do for now we do however have an issue let's say i want a certain model to be excluded from the site-wide search and right now there's no way to do that let's define an array where we'll put in all the models that we want to exclude i'll put a comment for now just for testing purposes and to bring our to exclude variable inside our filter function we need to use keyword to bring the variable into the scope and now in our return statement we'll add a new condition to make sure the class name is not in the to exclude array so we'll type not in array and the needle would be the full qualified name of the class where we can get it from the reflection get name method the hay stack would be the two exclude array and the third argument strict is telling php to compare the values in strict mode in other words using a triple equal sign instead of double equal and once again our filter condition here would read as the class should be a model and searchable and it is not inside the to exclude array let's test our code to see if it's working and we only get post in the result because comment is excluded and user does not have the searchable trait so if we remove comment from the to exclude array we will see comment and post inside the result so far so good let's move on the next part is to call the search scout function to do that we'll call the map function again and convert each class name into the search result so to code the search function we first need to resolve the class to do that we can use laravel's add function to instantiate the class again we need the fully qualified class name and now we're repeating ourselves just like in line 36 which is a big no no let's refactor this why don't we make this a function i'm going to call it model namespace prefix and it simply returns the app namespace followed by models and now we can replace the prefix with this function nice and clean now that we have resolved the model we can call the search function straight away the search function requires the keyword query parameter so let's pull that in into the function scope by using a use keyword and now we'll fetch all the search results by calling get right after the search method for each search result we want to include the match model and view link attribute to do that we can do another map on the search result and in the callback function we'll correct the match model and view link attribute correspondingly so to correct the match attribute we need to get all the searchable fields value and find the exact string position of the search keyword to get a field value we can call the only function from the model record but the only method accepts an array of fields name we can make use of the searchable fields constant in our model class but a constant also includes the id field which will create noise in our result and is not needed by the front-end user let's prepare the searchable fields so we can filter out the id field we can call array filter on the searchable fields class constant our filter condition here is to include everything other than the id field and i'm using arrow function here which is only available in php 7.4 and upwards if you're using a lower version of php you can still use the normal anonymous function here and back in our map function we will pull in the fields variable again into the scope using a use keyword and pass the fields variable into the only function so field's data should be an array and we should join the elements together into one big string once we've got this giant string containing all the searchable text we can now find the position of the keyword in this string we'll use the stringpost function on the lowercase serialize values and lowercase keyword the reason why we want to convert both of them into lowercase is to standardize the search just in case the user pass in values with different casing and get frustrated because they couldn't find the exact match anyway once we got a search position our goal here is to include the text neighboring the match to provide the end user a better search experience we also want to append a prepend a triple dot before or after our neighboring text to indicate there are more contents beyond what's being shown to the user just like how google did it in the search result so the search position could be false if php couldn't find the search position the string pause function will return false so that means we need to handle this situation we can use an if statement to make sure search position is a number now in the if block our ultimate goal is to extract a portion of the serialized values which centers around the match keyword to extract a string we'll use the php built-in function substring but to use a substring function we need a start position and a length parameter let's correct these numbers the starting number will be the search position minus some kind of buffer i'll put in 10 for now which means we'll start slicing the string 10 characters before the match and we need to make sure the starting number is not less than zero if it is negative we'll set start as zero the length of the slice should contain three parts the first part is the first section of the buffer the second part is the length of the keyword and the third part is the buffer after the keyword so in other words the length will be the length of the keyword plus two buffers so two multiplied by ten now we're hard coding this buffer value at the moment which makes it very easy for us to lose track of it in the future and that is a big no-no let's refactor it into a class constant so we can easily configure it in the future and next we should work out if we need to add a triple dot or not both at the front or the back if the starting position is larger than zero we should add the prefix we should add a postfix if the total slice length is less than the total string length which means slice hasn't reached the end of serialized values yet so there are more stuff after the end of the slide string so we should add a triple dot after it once we set up the condition we should just add the triple dot based on the condition to the slice string once that's done we are now ready to set the match attribute to our model record which is equal to the slice string if slice happens to be undefined though in other words php couldn't find a search position we'll set the match attribute to the first turning characters of the serialized values for the model attribute it will be easy we just need to set it to class name again we'll bring class name into the scope of this function and now for the view link let's create a new function for that in our call that function resolve model view link and it will save an argument which is our model and the idea of this function is that it receive a model and return the resource url that looks something like this for example if we have a post model it will look like post slash one however we're assuming all models in our app are using this convention for the view which is again a big no-no it'll be great if we have some sort of method that maps alternative url patterns let's do that we can create a new mapping array and the key of this array will be our model class and the value will be the alternative patterns for example let's say my comments models are using a url pattern that looks something like this okay now let's start writing the logic for this function first of all we need to get a fully qualified class name for the model once we got that we should check whether the class name exists inside our mapping array if yes we can just use that pattern and replace the id placeholders with the actual id otherwise we use the default convention just like we discussed up there with the default convention though we need to convert the class name into kebab casing as the class name will be in pascal casing alright let's get into it first of all to get a fully qualified class name we can call the getclassbuildingphp function for that to check whether this class is one of the key inside a mapping array we can simply use the has array helper function in laravel so the hash function accepts two arguments the first one is our target array and the second one is a key name if the key exists it will simply return true otherwise false so if model class exists inside mapping we'll simply replace the id placeholder with the actual id of the model once that's done we'll need to convert the whole string into a full app url with our app domain included in it again we can use the laravel url helper and that will do and now let's work on our default convention we first need to extract the model name from the fully qualified class name this will be straightforward we can just split model class by backslash and get the last element in a split we also want to polarize our model name just like our convention next to convert our model name into kebab casing again we can use laravel's helper functions for that we first need to convert it into a camera casing and then kebab why can't we convert into kebab straight away the reason is the kebab helper functions works really well with camo case string but not so much on other casing if you use kebab right away you might get some unexpected output okay now that our model name is ready let's convert it into a url again we'll call the tool function from the url facade okay back in the main function we just need to return the model record after we have finished setting the attributes okay now let's see how our results looks like in the browser i have prepared some dummy records beforehand and let's see where would this leads us to so my search term here is n and we do see some records in our results however we have a nested collection here with the third level being the actual model records that we want to show in the api response let's fix that we should flatten our results by one level so that our results will appear on the first level let's try that one more time and now we see all of our results showing up in the first level let's check whether our attributes are set up properly and it looks great our keyword match is in there and also the surrounding text looking good and the last piece of the puzzle will be creating a resource class for our search results let's go to our terminal and php artisan make resource i'll call it site search results and in the resource class we will return only four things the id of the model the match the model and the view link attributes and now let's go back to our controller and put our results into this bad boy and try this in our browser one more time and we see a beautifully printed json response along with all the data that we need and now we just need a front end to call our api i'll quickly create a very simple front end to show you how it looks like in the browser [Applause] and that's it we can now search everything from our app using this search box the source code of this project is in the description so feel free to check it out and if you don't feel like writing this on your own on every single new project i've also created a laravel package that does this all for you the link again is in the description that's it for now i hope that you have learned something new in this lesson and i'll see you again in the next video if you enjoy the content of this video don't forget to hit the like subscribe and the bell icon for more content to come it will really help me out thanks for the support [Music] you
Info
Channel: Acadea.io
Views: 7,773
Rating: undefined out of 5
Keywords: laravel, full text search, site wide search, php, laravel scout
Id: yuG1kS9WFz0
Channel Id: undefined
Length: 18min 39sec (1119 seconds)
Published: Tue Oct 27 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.