Adding Autocomplete to Your Laravel Applications With MongoDB Atlas Search
Rate this tutorial
Implementing a search feature has become hugely important for every web app that values user experience, as users can search for what they need to see without scrolling endlessly.
In this tutorial, we will build a movie application leveraging MongoDB Atlas Search with Laravel (a leading PHP framework) to build a rich text-based search feature that allows users to search for movies by typing a few letters.
In this tutorial, we will build a movie application leveraging MongoDB Atlas Search with Laravel (a leading PHP framework) to build a rich text-based search feature that allows users to search for movies by typing a few letters.
MongoDB's flexible schema, Atlas Search, and powerful querying capabilities, combined with Laravel's expressive syntax, enable us to implement advanced search functionalities quickly.
To set up our MongoDB environment, create a database cluster, set database access, and get the database connection string, we must follow the below instructions from the MongoDB official documentation.
Once we have our cluster set up, we need to load some sample data we will work with for this implementation and get our connection string.
Your connection string should look like this:
mongodb+srv://user:password@cluster0.xxxxx.mongodb.net
If you complete the above steps, the sample data should be loaded in your cluster. Otherwise, check out the documentation on how to load sample data.
After successfully loading the data, we should have a database called sample_mflix within our cluster. We will work with the movies collection in the database.
With the database set up, let’s create the Laravel project with Composer. To continue, you want to make sure you have PHP, Laravel, Node, npm, Composer, and finally, the MongoDB PHP extension all properly set up. The following links will come in handy.
Check out the instructions for the MongoDB and Laravel integration. They explain how to configure a Laravel-MongoDB development environment. We'll cover the Laravel application creation and the MongoDB configuration below.
With our development environment working, to create a Laravel project, we will use Composer. Run the below code in the term in your preferred directory.
1 composer create-project Laravel/Laravel movieapp
This command will create a new Laravel project in the folder movieapp. After completing installation, your folder structure should look as below.
From the terminal, run the below code to start the server:
1 cd movieapp 2 php artisan serve
Once the server is running, it should be available at `http://localhost:8000/`. You should see the Laravel starter page, as shown below.
Also, in a new terminal window, run the below command to start the front-end server.
1 npm install 2 npm run dev
With our server up and running, let's connect to our MongoDB cluster.
To connect the server to the MongoDB cluster we created earlier, follow as below:
- Firstly, composer lets you add the Laravel MongoDB package to the application. In the command prompt, go to the project's directory and run the command below.
1 composer require mongodb/Laravel-mongodb this will add the MongoDB package to the vendor directory - Let us use composer to add another package called mongodb/builder. We will be using this to build an aggregation pipeline later in this guide. Run the below command to add mongodb/builder.
1 composer require mongodb/builder:^0.2 - Navigate to the .env. Let’s update the DB_CONNECTION value and add a DB_URL as below:
1 DB_CONNECTION=mongodb 2 DB_URI=mongodb_connection_string_here
Update the text mongodb_connection_string_here with your database connection string.
4. Navigate to the config/database.php file and update the connection array as below:
4. Navigate to the config/database.php file and update the connection array as below:
1 'connections' => [ 2 'mongodb' => [ 3 'driver' => 'mongodb', 4 'dsn' => env('DB_URI'), 5 'database' => 'sample_mflix', 6 ],
5. Still on the config/database.php file, update the default string as:
1 'default' => env('DB_CONNECTION'),
This is to set MongoDB as the default connection for the application. With these variables updated, we should be able to connect to MongoDB successfully,
Let's create a route in the /routes/web.php:
1 2 use Illuminate\Support\Facades\Route; 3 // Import the Request class 4 use Illuminate\Http\Request; 5 // Import the DB facade 6 use Illuminate\Support\Facades\DB; 7 Route::get('/', function () { 8 return view('welcome'); 9 }); 10 //add ping route 11 Route::get('/connect', function (Request $request) { 12 $connection = DB::connection('mongodb'); 13 $msg = 'MongoDB is accessible!'; 14 try { $connection->getMongoClient()->selectDatabase($connection->getDatabaseName())->command(['ping' => 1]); 15 } catch (\Exception $e) { 16 $msg = 'MongoDB is not accessible. Error: ' . $e->getMessage(); 17 } 18 return response()->json(['msg' => $msg]); 19 });
In the terminal, run php artisan route
Let's create an Eloquent model for our MongoDB database named "Movie" since we will be working with a collection named movies. By convention, the "snake case," the plural name of the class, will be used as the collection name unless another name is explicitly specified. So, in this case, Eloquent will assume the Movie model stores documents in the movies collection. Run the command from the project's directory to create the Movie model.
1 php artisan make:model Movie
Once the command has finished running, it will create /app/Models/Movie.php. Open the file and update as below:
1 <?php 2 namespace App\Models; 3 use MongoDB\Laravel\Eloquent\Model; 4 class Movie extends Model 5 { 6 protected $connection = 'mongodb'; 7 }
With the model created, let's display some of the movies on our home page.
To do this, update the /routes/web.php:
1 <?php 2 use Illuminate\Support\Facades\Route; 3 use App\Models\Movie; 4 Route::get('/', function () { 5 $movies = Movie::limit(20)->get(); // Retrieve only 20 movies 6 return view('welcome', [ 7 'movies' => $movies 8 ]); 9 });
Then, update the body tag of app/resources/views/welcome.blade.php, as below.
1 <body class="font-sans antialiased dark:bg-black dark:text-white/50"> 2 <div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50"> 3 <img id="background" class="absolute -left-20 top-0 max-w-[877px]" 4 src="https://Laravel.com/assets/img/welcome/background.svg" alt="Laravel background"/> 5 <div 6 class="relative min-h-screen flex flex-col items-center justify-center selection:bg-[#FF2D20] selection:text-white"> 7 <div class="relative w-full max-w-2xl px-6 lg:max-w-7xl"> 8 <div id="movie-list" class=' w-full flex gap-6 justify-around items-center flex-wrap'> 9 </div> 10 </div> 11 </div> 12 <script> 13 let movies = @json($movies); 14 displayMovies(movies) 15 function displayMovies(movies) { 16 const movieListDiv = document.getElementById('movie-list'); 17 movieListDiv.innerHTML = movies.map(movie => ` 18 <div class="movie-card max-w-sm bg-green-500 border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"> 19 <div class="h-[400px]"> 20 <img class="rounded-t-lg object-cover w-full h-full" src="${movie.poster}" alt="${movie.title}" /> 21 </div> 22 <div class="p-5"> 23 <a href="#"> 24 <h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white line-clamp-1"> 25 ${movie.title} 26 </h5> 27 </a> 28 <p class="mb-3 font-normal text-gray-700 dark:text-gray-400 line-clamp-2">${movie.plot}</p> 29 <a href="#" 30 class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> 31 See more 32 </a> 33 </div> 34 </div> 35 `).join(''); 36 } 37 </script> 38 </body>
Refresh your application on http://localhost:8000. You should have a list of movies displayed on the screen, as below.
Did you get an error, E11000 duplicate key error collection: sample_mflix.sessions index: user_id_1 dup key: { user_id: null }? In this case, head over to the sessions collection in sample_mflix database, clear the document in it, and try again.
With the application running, let's head over to the MongoDB Atlas dashboard to create an Atlas search index. But before that…
To implement the feature such that we can search movies by their title, let’s create a search index from the MongoDB Atlas dashboard.
From the previously created cluster, click on Browse collections, navigate to the Atlas Search tab, and click on Create index on the right side of the search page. On this screen, select JSON editor under Atlas Search, click Next to proceed, add an index name (in our case, movie_search), select the movies collection from the sample_mflix database, and update the JSON editor as below. Click Next.
1 { 2 "mappings": { 3 "dynamic": false, 4 "fields": { 5 "title": { 6 "type": "string" 7 } 8 } 9 } 10 }
Next, let's add another search index, but this time, the type will be autocomplete. We will use it to implement a feature such that when a user types in the input box, it will suggest possible movie titles.
Create another index, give it a name (in our case, movie_title_autocomplete), and update the JSON editor, as below:
1 { 2 "mappings": { 3 "dynamic": false, 4 "fields": { 5 "title": { 6 "type": "autocomplete" 7 } 8 } 9 } 10 }
Let’s create two functions called searchByTitle and autocompleteByTitle within the Movie model class. These functions will implement the search and the autocomplete features, respectively.
Therefore, let’s update the app/Models/Movie.php file as below.
1 <?php 2 namespace App\Models; 3 use Illuminate\Support\Collection; 4 use MongoDB\Laravel\Eloquent\Model; 5 6 class Movie extends Model 7 { 8 protected $connection = 'mongodb'; 9 10 public static function searchByTitle(string $input): Collection 11 { 12 return self::aggregate() 13 ->search([ 14 'index' => 'movie_search', 15 'compound' => [ 16 'must' => [ 17 [ 18 'text' => [ 19 'query' => $input, 20 'path' => 'title', 21 'fuzzy' => ['maxEdits' => 2] // Adding fuzzy matching 22 ] 23 ] 24 ] 25 ] 26 ]) 27 ->limit(20) 28 ->project(title: 1, genres: 1, poster: 1, rated: 1, plot: 1) 29 ->get(); 30 } 31 32 public static function autocompleteByTitle(string $input): Collection 33 { 34 return self::aggregate() 35 ->search([ 36 'index' => 'movie_title_autocomplete', 37 'autocomplete' => [ 38 'query' => $input, 39 'path' => 'title' 40 ], 41 'highlight' => [ 42 'path' => ['title'] 43 ] 44 ]) 45 ->limit(5) // Limit the result to 5 46 ->project(title: 1, highlights: ['$meta' => 'searchHighlights']) 47 ->get(); 48 } 49 }
Note: Putting the code that makes the search query in the model class separates the data access layer from the http controllers. This makes the code more testable.
Let’s create a controller to implement the search API in the project. Let's run the code below to create a controller.
1 php artisan make:controller SearchController
This command will create an app/Http/Controllers/SearchController.php file. Let's update the file as below.
1 <?php 2 namespace App\Http\Controllers; 3 use App\Models\Movie; 4 use Illuminate\Http\JsonResponse; 5 6 class SearchController extends Controller 7 { 8 public function search($search): JsonResponse 9 { 10 // Define the aggregation based on the search conditions 11 if (!empty($search)) { 12 $items = Movie::searchByTitle($search); 13 return response()->json($items, 200); 14 } 15 return response()->json(['error' => 'conditions not met'], 400); 16 } 17 }
Next, let's create an API route.
Navigate to app/routes/web.php:
1 // import the SearchController 2 use App\Http\Controllers\SearchController; 3 4 Route::get('/search/{search}', [SearchController::class, 'search']);
Navigate to the app/Http/Controllers/SearchController.php file and add the below function after the end of the `search` function.
1 public function autocomplete($param): JsonResponse 2 { 3 try { 4 $results = Movie::autocompleteByTitle($param); 5 return response()->json($results, 200); 6 } catch (\Exception $e) { 7 return response()->json(['error' => $e->getMessage()], 500); 8 } 9 } 10 }
Next, let's create an API route. Navigate to app/routes/web.php.
1 Route::get('/autocomplete/{param}', [SearchController::class, 'autocomplete']);
Test the API by calling it like so:
1 http://localhost:8000/autocomplete/hello
To see these implements in action, let's update the body tag of the app/resources/views/welcome.blade.php as below.
Get the complete code snippet on GitHub.
Get the complete code snippet on GitHub.
1 <body class="font-sans antialiased dark:bg-black dark:text-white/50"> 2 <script> 3 let debounceTimer; 4 let movies = @json($movies); 5 displayMovies(movies) 6 7 function handleSearch(event) { 8 const query = event.target.value; 9 // Clear the previous debounce timer 10 clearTimeout(debounceTimer); 11 // Set up a new debounce timer 12 debounceTimer = setTimeout(() => { 13 if (query.length > 2) { // Only search when input is more than 2 characters 14 titleAutocomplete(query); 15 } 16 }, 300); 17 } 18 19 async function titleAutocomplete(query) { 20 try { 21 const response = await fetch(`/autocomplete/${encodeURIComponent(query)}`); 22 const movies = await response.json(); 23 displayResults(movies); 24 } catch (error) { 25 console.error('Error fetching movies:', error); 26 } 27 } 28 29 async function fetchMovies(query) { 30 try { 31 const response = await fetch(`/search/${encodeURIComponent(query)}`); 32 const movies = await response.json(); 33 displayMovies(movies); 34 displayResults([]) 35 } catch (error) { 36 console.error('Error fetching movies:', error); 37 } 38 } 39 40 function displayResults(movies) { 41 const resultsDiv = document.getElementById('search-results'); 42 resultsDiv.innerHTML = movies.map(movie => ` <div onclick="fetchMovies('${movie.title}')" class='select-none text-gray-600 cursor-pointer flex items-center gap-[10px]'> 43 <div> 44 ${movie.title} 45 </div> 46 </div>`).join(''); 47 } 48 49 50 function displayMovies(movies) { 51 const movieListDiv = document.getElementById('movie-list'); 52 movieListDiv.innerHTML = movies.map(movie => ` 53 <div class="movie-card max-w-sm bg-green-500 border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"> 54 <div class="h-[400px]"> 55 <img class="rounded-t-lg object-cover w-full h-full" src="${movie.poster}" alt="${movie.title}" /> 56 </div> 57 <div class="p-5"> 58 <a href="#"> 59 <h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white line-clamp-1"> 60 ${movie.title} 61 </h5> 62 </a> 63 <p class="mb-3 font-normal text-gray-700 dark:text-gray-400 line-clamp-2">${movie.plot}</p> 64 <a href="#" 65 class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> 66 See more 67 <svg class="rtl:rotate-180 w-3.5 h-3.5 ms-2" aria-hidden="true" 68 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10"> 69 <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" 70 stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9" /> 71 </svg> 72 </a> 73 </div> 74 </div> 75 `).join(''); 76 } 77 </script> 78 </body>
In the above changes:
- We added a function called displayMovies, which takes movies as an argument. It will render movie cards to the screen based on the movies list.
- Then, we have a function called handleSearch which is an oninput event handler for the search input box.
- Within the handleSearch function, we have a function called titleAutocomplete, which fetches and displays data from the autocomplete API endpoint.
- Then, we have the fetchMovies function, which fetches data from the search API endpoint within which we call the displayMovies function to display the movie’s response for the API.
It is crucial to make it easy for your users to find what they are looking for on a website to have a great user experience. In this guide, I showed you how I created a text search for a movie application with MongoDB Atlas Search. This search will allow users to search for movies by their title.
Atlas Search is a full-text search engine that enables developers to implement rich search functionality into their applications. It allows users to search large quantities of data quickly and efficiently.
Do you have questions or comments? Let's continue the conversation! Head over to the MongoDB Developer Community we'd love to hear from you.
Happy coding.