Skip to content

Latest commit

 

History

History
214 lines (156 loc) · 7.02 KB

searching-for-bobby-fisher.md

File metadata and controls

214 lines (156 loc) · 7.02 KB

Searching for Bobby Fisher with Laravcel 5

In modern web applications, a common requirement is a search feature. Clients are spoiled by Google and other search engines and expect a powerful search experience in their own products. In this tutorial, we'll cover how to search your user base and sequentially make yourself or your clients happy.

If you worked on a medium-sized or larger application, assuming you have a typical "users" table, you came up with a solution yourself like:

public function scopeSearchByKeyword($query, $keyword)
{
    if ($keyword!='') {
        $query->where(function ($query) use ($keyword) {
            $query->where("firstname", "LIKE","%$keyword%")
                ->orWhere("lastname", "LIKE", "%$keyword%")
                ->orWhere("email", "LIKE", "%$keyword%")
                ->orWhere("phone", "LIKE", "%$keyword%");
        });
    }
    return $query;
}

This will certainly work for one keyword, but you'll face problems when you try to search by multiple keywords like Bobby Fischer. So you dig deeper into your toolbox and write something like:

$users = User::where(function ($q) use ($query) {
    $q->where(DB::raw('CONCAT( firstname, " ", lastname)'), 'like', '%' . $query . '%')
    ->orWhere(DB::raw('CONCAT( lastname, " ", firstname)'), 'like', '%' . $query . '%')
    ->orWhere('email', 'like', '%' . $query . '%')
});

As you see, this will cover the case Bobby Fischer but will have several downsides. This approach doesn't scale very well. If the wildcard % operator is both on the left and right side of the query %query%, the internal index of the database cannot be leveraged, which means that the DB engine needs to go through every single row to see if there's a match, and that's bad... not to mention slow!

If you introduce more fields, you'll have even more permutations in your code so you'll end up with a nonperformant and nonreadable query.

The solution to this problem is a package written in pure PHP that deals with this stuff and lets you do some cool things. Laravel also introduced a driver based solution for full-text search which works nicely with TNTSearch.

The installation process is easy and can be done with the following command

composer require teamtnt/laravel-scout-tntsearch-driver

After that, add the service provider to app/config/app.php:

// config/app.php 'providers' => [ // ... Laravel\Scout\ScoutServiceProvider::class, TeamTNT\Scout\TNTSearchScoutServiceProvider::class, ],

Add SCOUT_DRIVER=tntsearch to your .env file

Then you should publish scout.php configuration file to your config directory

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

In your config/scout.php add:

'tntsearch' => [
    'storage'  => storage_path(), //place where the index files will be stored
    'fuzziness' => env('TNTSEARCH_FUZZINESS', false),
    'fuzzy' => [
        'prefix_length' => 2,
        'max_expansions' => 50,
        'distance' => 2
    ],
    'asYouType' => false,
    'searchBoolean' => env('TNTSEARCH_BOOLEAN', false),
],

The first thing is creating the index and the second thing is answering the search queries using the index we created.

We'll create a laravel command that will do the indexing for us:

namespace App\Console\Commands;

use Config;
use Illuminate\Console\Command;
use TNTSearch;

class IndexUsers extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'index:users';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Index the users table';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {

        $indexer = TNTSearch::createIndex('users.index');
        $indexer->query('SELECT id, firstname, lastname, email, phone, bio FROM users;');
        $indexer->run();
    }
}

Once we have this command we can run php artisan index:users. This will create a file in your storage folder called users.index. The index is now complete and we can do queries against it.

Another approach to achieve the same thing is to add a searchable trait to your model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class User extends Model
{
    use Searchable;

    public $asYouType = true;
    
    /**
     * Get the indexable data array for the model.
     *
     * @return array
     */
    public function toSearchableArray()
    {
        $array = $this->toArray();

        // Customize array...

        return $array;
    }
}

And then run:

php artisan scout:import App\\Post

That's it. Doing the search is simple. We'll do this in our UserController:

public function index(Request $request)
{
    $users = User::search($request->input('q'))->get();

    return view('users.index', compact('users'));
}

This controller assumes that you have a simple search form which submits a query in a field called q.

The searchBoolean method is a very powerful feature and if you are familiar with some basic boolean algebra you'll understand right away how it works.

Every space represents an AND operator so when you type Bobby Fisher you are actually asking for every record that contains the words Bobby and the word Fisher. It translates to Bobby AND Fisher.

If you want results that contain either Bobby or Fisher you would write Bobby or Fisher.

What about negation, ie. if you want to get all Fishers that aren't Bobbies? Simple, Bobby -Fisher.

The dash - represents the negation.

You can also type part of an email like gmail.com which will return all users that have a Gmail address. I'm sure you can think of many more examples.

Updating the index manually isn't necessary because scout will do it automatically.

What about scaling you may ask? Will it handle more than 10k users? Don't worry, it will scale fine even if you have millions of users.

This is a simple use tutorial to get you started with TNTSearch package which is often more than enough to satisfy your or your client needs for searching.

In the upcoming versions of the package, you can expect new features like "weighting schemes", "fuzzy searching" and similar advanced features.

If you like the package please star it on Github, it means a lot to have support from the community and if you have suggestions please let us know via Github or in comments.

For other cool tutorials subscribe to our newsletter below.