Tìm kiếm thông minh với Typeahead trong ứng dụng Laravel

Trong những website lớn, các phần tìm kiếm hay nhập liệu rất cần những tính năng thông minh như gợi ý dựa trên các từ nhập vào, nó giúp cho nâng cao trải nghiệm người dùng, giúp tìm kiếm và nhập liệu trở lên đơn giản hơn. Google là một minh chứng không cần phải bàn cãi cho vấn đề này. Những năm đầu của thế kỉ 21, Google xuất hiện với chỉ duy nhất một ô tìm kiếm và tính năng gợi ý ngay lập tức khi từ khóa được đánh vào, nó đã giúp người dùng định hướng được ngay khi chỉ gõ vào 1-2 từ trong từ khóa. Ở thời điểm đó, gợi ý khi tìm kiếm là một tính năng phức tạp và xa xỉ, nhưng khi công nghệ phần mềm phát triển, đặc biệt với phần mềm mã nguồn mở, những tính năng như vậy thật đơn giản để thực hiện. Bài viết này sẽ hướng dẫn bạn thực hiện các tìm kiếm thông minh hay gợi ý nhập liệu sử dụng thư viện Typeahead là một gói phần mềm mã nguồn mở của Twitter trong các ứng dụng Laravel.

Ví dụ về typeahead

Trong bài viết này chúng ta sẽ thực hiện một ví dụ tìm kiếm thông minh thông tin khách hàng bằng cách tạo ra dữ liệu mẫu khoảng 10,000 khách hàng. Như bạn thấy khi tìm kiếm hệ thống sẽ gợi ý các bản ghi trong database rất nhanh giúp người dùng có thể lựa chọn luôn chính xác người dùng. Typeahead sử dụng kết hợp Bloodhound cho tốc độ rất nhanh mặc dù dữ liệu là 10k bản ghi.

1. Giới thiệu về Typeahead

  • Typeahead.js là một thư viện Javascript rất linh hoạt, nó có thể làm nền tảng tốt để xây dựng các tính năng tìm kiếm gợi ý thông minh. Typeahead bao gồm hai thành phần:
    Typeahead: Phần chuyên xử lý giao diện người dùng

    • Hiển thị gợi ý đến người dùng ngay khi họ nhập liệu
    • Hiển thị các gợi ý ngay trên ô nhập liệu
    • Hỗ trợ các tùy chỉnh giao diện linh hoạt
    • Highlight các từ khóa trùng khớp trong phần gợi ý
    • Kích hoạt các sự kiện tùy chỉnh cho phép mở rộng các xử lý
  • Bloodhound: Bộ máy gợi ý nâng cao
    • Cho phép các dữ liệu được hardcode
    • Lấy dữ liệu từ trước để giảm độ trễ khi gợi ý
    • Sử dụng Local Storage giảm số lượng các request đến máy chủ.
    • Sử dụng rate limit và bộ đệm cho các request đến máy chủ làm giảm nhẹ tải dữ liệu

Bộ máy gợi ý Bloodhound sẽ được sử dụng để tính toán kết quả với các truy vấn cho trước và Typeahead sẽ sử dụng để render ra mã HTML. Cả hai thành phần này là độc lập, trong bài viết này chúng ta sẽ sử dụng cả hai để xây dựng công cụ tìm kiếm gợi ý thông minh.

1.1 Cài đặt Typeahead

Trước khi đi vào sử dụng Typeahead chúng ta cần cài đặt gói thư viện này, có ba cách thức để cài đặt.

Sử dụng npm

npm i typeahead

Tải gói thư viện dạng file zip

Vào đường dẫn Github của Typeahead chọn Clone or download và click vào Download zip. Giải nén ra chúng ta sẽ thấy trong thư mục dist có những file như sau:

  • bloodhound.js (chỉ có thành phần Bloodhound).
  • typeahead.bundle.js (Bao gồm cả Typeahead và Bloodhound).
  • typeahead.jquery.js (chỉ có thành phần Typeahead).

Các file có thêm min.js là các file được tối ưu hóa dung lượng.

Tích hợp thông qua CDN

Sử dụng CDN giúp file có thể được tải về nhanh hơn, tuy nhiên cũng gặp phải một vấn đề là khi cá mập cắn cáp, internet chập chờn nếu các CDN không có server ở gần Việt Nam thì các thư viện này rất khó để tải về.

jQuery: https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js

Typeahead: https://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.11.1/typeahead.bundle.min.js

Chú ý: Typeahead yêu cầu jquery phiên bản từ 1.9 trở lên.

Trong bài viết này chúng ta sẽ sử dụng cách thứ ba cho nhanh, với dự án lớn nên sử dụng cách 1 để tối ưu hóa các tài nguyên với Laravel Mix.

1.2 Khởi tạo Typeahead

  • Typeahead có nhiều cách khởi tạo và sau đây là cách khởi tạo hay dùng nhất jQuery#typeahead(options, [*datasets]). Tính năng typeahead được áp dụng cho các input dạng text input[type=”text”] có hai tham số cho khởi tạo:
    options là các tùy chọn cấu hình, một số giá trị cần quan tâm như sau:

    • highlight: thêm thẻ <strong> vào các từ trùng khớp trong phần gợi ý. Mặc định là false.
    • hint: hiển thị cả từ gợi ý trong ô nhập liệu, mặc định true.
    • minLength: Số ký tự tối thiểu cần nhập khi tính năng gợi ý được bắt đầu, mặc định là 1.
    • classNames: Cho phép sử dụng tên class khác với mặc định.
  • dataset: một typeahead có thể có nhiều dataset, ví dụ khi bạn tìm kiếm trong một trang bán hàng có thể trả về gợi ý cho cả sản phẩm và các tin tức liên quan đến sản phẩm. Các dataset có một số các tùy chọn cấu hình như sau:
    • name: tên của dataset.
    • source: nguồn dữ liệu dùng cho gợi ý, có thể là một instance của Bloodhound, như ở phần đầu chúng ta có nói Typeahead chỉ xử lý giao diện người dùng và Bloodhound với là bộ máy thực hiện các gợi ý.
    • limit: Số gợi ý tối đa sẽ được hiển thị, mặc định là 5.
$("#navbar-search-input").typeahead({
    hint: true,
    highlight: true,
    minLength: 1
});

1.3 Khởi tạo Bloodhound

Bloodhound là bộ máy gợi ý cho Typeahead.js, sử dụng thành phần này mang lại nhiều tính năng nâng cao hơn vì nó có thể lấy dữ liệu từ một nguồn remote và sử dụng bộ đệm để tăng tốc.

var engine = new Bloodhound({
    remote: {
        url: 'api/customer?q=%QUERY%',
        wildcard: '%QUERY%'
    },
    datumTokenizer: Bloodhound.tokenizers.whitespace('q'),
    queryTokenizer: Bloodhound.tokenizers.whitespace
});

Chúng ta sẽ thiết lập đường dẫn /find?q= trong phần Laravel, datumTokenizer cần một mảng JSON. Như vậy, chúng ta đã có dữ liệu và có thể sử dụng nó cho thiết lập source của typeahead như sau:

source: engine.ttAdapter()

1.4 Tạo mẫu cho các gợi ý

Typeahead cho phép sử dụng các template để thay đổi kiểu mẫu cho các gợi ý, bạn cũng có thể sử dụng bootstrap để style:

templates: {
    empty: [
        '<div class="list-group search-results-dropdown"><div class="list-group-item">Không có kết quả phù hợp.</div></div>'
    ],
    header: [
        '<div class="list-group search-results-dropdown">'
    ],
    suggestion: function (data) {
         return '<a href="' + data.id + '" class="list-group-item">' + data.name + '</a>'
    }
}

1.5 Code hoàn chỉnh cho Typeahead.js

jQuery(document).ready(function($) {
    var engine = new Bloodhound({
        remote: {
            url: 'api/customer?q=%QUERY%',
            wildcard: '%QUERY%'
        },
        datumTokenizer: Bloodhound.tokenizers.whitespace('q'),
        queryTokenizer: Bloodhound.tokenizers.whitespace
    });

    $(".search-input").typeahead({
        hint: true,
        highlight: true,
        minLength: 1
    }, {
        source: engine.ttAdapter(),
        name: 'usersList',
        templates: {
            empty: [
                '<div class="list-group search-results-dropdown"><div class="list-group-item">Không có kết quả phù hợp.</div></div>'
            ],
            header: [
                '<div class="list-group search-results-dropdown">'
            ],
            suggestion: function (data) {
                return '<a href="' + data.id + '" class="list-group-item">' + data.name + '</a>'
      }
        }
    });
});

2. Xây dựng backend với Laravel

Phần 2 chúng ta sẽ xây dựng ứng dụng Laravel

2.1 Cài đặt môi trường Laravel

Tham khảo công cụ Laragon cài đặt nhanh môi trường Laravel, chúng ta tạo ra một môi trường test có tên là typeahead và Laragon tự động tạo ra tên miền ảo typeahead.dev. Laragon cũng tự động tạo ra database tên typeahead cho chúng ta. Việc đầu tiên là thiết lập file .env:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=typeahead
DB_USERNAME=root
DB_PASSWORD=secret

2.2 Tạo bảng Customer và dữ liệu mẫu

Tạo ra Model Customer cùng với file migrate thông qua câu lệnh artisan make:model (Xem thêm Laravel Artisan là gì?)

D:\Laragon\www\typeahead
λ php artisan make:model Customer -m
Model created successfully.
Created Migration: 2017_09_04_084521_create_customers_table

Chỉnh sửa file migrate xxx_create_customers_table.php trong thư mục database\migrations:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCustomersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 255);
            $table->string('address', 255);
            $table->string('phone', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('customers');
    }
}

Thực hiện tạo ra bảng customers trong database với câu lệnh artisan migrate. (Tìm hiểu thêm về Laravel Migration và Laravel Seeding tạo database và dữ liệu test)

D:\Laragon\www\typeahead
λ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2017_09_04_084521_create_customers_table
Migrated:  2017_09_04_084521_create_customers_table

Có nhiều các bảng khác được tạo ra do các bảng này được sử dụng cho xác thực người dùng (xem Laravel Authentication xác thực người dùng thật đơn giản). Tiếp theo chúng ta sẽ sử dụng Laravel Seeding để tạo ra 10000 dữ liệu customer mẫu trong database.

D:\Laragon\www\typeahead
λ php artisan make:seeder CustomersTableSeeder
Seeder created successfully.

Tạo file CustomerFactory.php trong thư mục database\factories:

<?php
use Faker\Generator as Faker;

$factory->define(App\Customer::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'address' => $faker->address,
        'phone' => $faker->phoneNumber
    ];
});

Class này sử dụng Faker để tạo ra dữ liệu test. Tiếp theo chúng ta tạo file Seeder:

D:\Laragon\www\typeahead
λ php artisan make:seeder CustomersTableSeeder
Seeder created successfully.

Và thêm đoạn mã sau vào file CustomersTableSeeder.php trong thư mục database\seeds:

<?php
use Illuminate\Database\Seeder;

class CustomersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(Customer::class, 10000)->create();
    }
}

và khai báo sử dụng Seeder này trong file database\seeds\DatabaseSeeder.php:

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call(CustomersTableSeeder::class);
    }
}

Thực hiện tạo ra 10000 dữ liệu khách hàng mẫu với câu lệnh artisan db:seed

D:\Laragon\www\typeahead
λ php artisan db:seed
Seeding: CustomersTableSeeder

2.3 Route

Chúng ta đã tạo ra bảng customer với 10 nghìn dữ liệu khách hàng mẫu được đưa vào, tiếp theo chúng ta cần tạo ra một đường dẫn để thực hiện tìm kiếm khách hàng và trả về dữ liệu dạng JSON cho truy vấn Bloodhound, thêm route sau đây vào file routes\api.php:

Route::get('find', 'SearchController@find');

Tạo thêm một route trong routes\web.php để hiển thị thông tin chi tiết của khách hàng, ở đây chỉ thực hiện in ra màn hình thông tin mà không có tạo view, coi như bài tập thêm cho các bạn :).

Route::get('customer/{id}', function() {
   $customer = Customer::find($id);
   return $customer->name . '@' . $customer->phone . '-' . $customer->address;
});

2.4 Controller

Tạo controller SearchController bằng lệnh artisan:

D:\Laragon\www\typeahead
λ php artisan make:controller SearchController
Controller created successfully.

Thêm phương thức find vào SearchController.php trong app\Http\Controllers:

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Customer;

class SearchController extends Controller
{
    public function find(Request $request) {
    	$customer = Customer::where('name', 'like', '%' . $request->get('q') . '%')->get();
    	return response()->json($customer);
    }
}

Vào đường dẫn http://typeahead.dev/customer?q=jo chúng ta sẽ có kết quả là dữ liệu dạng JSON:

Dữ liệu cho Bloodhound dạng JSON

2.5 View

Tiếp đến chúng ta thay đổi welcome view nằm trong thư mục resources\views:

<!DOCTYPE html>
<html lang="vi">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Tìm kiếm thông minh sử dụng Typeahead trong ứng dụng Laravel">
        <meta name="author" content="FirebirD ['www.allaravel.com']">
        <title>Tìm kiếm thông minh trong Laravel sử dụng Typeahead - Allaravel.com</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!--[if lt IE 9]>
        <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
        <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
        <![endif]-->
        <style type="text/css">html{position:relative;min-height:100%;}body{margin-bottom:60px;}.footer{position:absolute;bottom:0;width:100%;height:60px;background-color:#f5f5f5;}body>.container{padding:60px 15px 0;}.container.text-muted{margin: 20px 0;}.footer>.container{padding-right:15px;padding-left:15px;}code{font-size:80%;}</style>
    </head>
    <body>
        <!-- Fixed navbar -->
        <nav class="navbar navbar-default navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="https://allaravel.com">Typeahead</a>
                </div>
                <div id="navbar" class="collapse navbar-collapse">
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="#">Home</a></li>
                        <li><a href="#about">About</a></li>
                        <li><a href="#contact">Contact</a></li>
                    </ul>
                </div><!--/.nav-collapse -->
            </div>
        </nav>

        <!-- Begin page content -->
        <div class="container">
            <div class="page-header">
                <h3>Ví dụ tìm kiếm thông minh sử dụng typeahead.js trong ứng dụng Laravel - Allaravel.com</h3>
            </div>
            <div class="row">
                <div class="col-md-12">
                    <form class="form-inline typeahead">
                        <div class="form-group">
                            <input type="name" class="form-control search-input" id="name" autocomplete="off" placeholder="Nhập tên khách hàng">
                        </div>
                        <button type="submit" class="btn btn-default">Tìm kiếm</button>
                    </form>
                </div>
            </div>
        </div>

        <footer class="footer">
            <div class="container">
                <p class="text-muted">Example in <a href="https://allaravel.com">allaravel.com</a></p>
            </div>
        </footer>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.11.1/typeahead.bundle.min.js"></script>
        <script>
            jQuery(document).ready(function($) {
                var engine = new Bloodhound({
                    remote: {
                        url: 'api/customer?q=%QUERY%',
                        wildcard: '%QUERY%'
                    },
                    datumTokenizer: Bloodhound.tokenizers.whitespace('q'),
                    queryTokenizer: Bloodhound.tokenizers.whitespace
                });

                $(".search-input").typeahead({
                    hint: true,
                    highlight: true,
                    minLength: 1
                }, {
                    source: engine.ttAdapter(),
                    name: 'usersList',
                    templates: {
                        empty: [
                            '<div class="list-group search-results-dropdown"><div class="list-group-item">Không có kết quả phù hợp.</div></div>'
                        ],
                        header: [
                            '<div class="list-group search-results-dropdown">'
                        ],
                        suggestion: function (data) {
                            return '<a href="customer/' + data.id + '" class="list-group-item">' + data.name + '</a>'
                        }
                    }
                });
            });
        </script>
    </body>
</html>

Kết quả khi vào http://typeahead.dev và thực hiện tìm kiếm khách hàng chúng ta được như sau:

Kết quả áp dụng typeahead trong ứng dụng Laravel

3. Lời kết

Với việc sử dụng Typeahead trong ứng dụng Laravel, trải nghiệm người dùng được nâng cao hơn. Typeahead không chỉ sử dụng trong các phần tìm kiếm mà chúng ta có thể sử dụng trong các form nhập liệu giúp gợi ý thông tin nhập liệu. Hi vọng bài viết sẽ giúp ích cho các bạn trong các dự án riêng sử dụng Laravel, có bất kỳ thắc mắc hoặc góp ý các bạn comment cuối bài nhé.

10 thoughts on “Tìm kiếm thông minh với Typeahead trong ứng dụng Laravel

  1. Cám ơn bạn. Bài viết rất hay và công phu.
    Tuy nhiên mình để thế này thì không work:
    DB_PASSWORD=secret

    Đổi sang thế này thì work:
    DB_PASSWORD=

    1. Trời đất, vấn đề cơ bản nhất khi học Laravel là cấu hình cho DB mà bạn cũng chưa nắm rõ?
      Phần DB_PASSWORD tùy thuộc vào cấu hình bạn sử dụng ở local. Cứ copy paste vô tội vạ thì sao mà workd được.

      1. Cái nớ tùy vào cấu hình dưới local của bạn mà.. từ từ qua nhưng chương này bạn quay lại từ đầu để có nền tốt nhất khi đó bạn sẽ hiểu mọi thứ sau này một cách đơn giản hơn!

  2. Bài viết hay, nhưng cần thêm phần tối ưu hóa. Dữ liệu có 10K bản ghi cũng không phải quá nhiều, tôi test thử 500K bản ghi thấy hơi chậm khi gợi ý. Ad có phương án nào tối ưu hơn nữa không?

    1. Có mấy gợi ý về tối ưu bạn có thể thử thực hiện:
      1. Phương án đầu tiền nghĩ đến khi thực hiện truy vấn chậm là cần phải đánh index cho cơ sở dữ liệu. Phần đánh index cũng cần phải lựa chọn một cách mềm dẻo các kiểu dữ liệu khác nhau.
      2. Phương án hai là tối ưu hóa truy vấn. Do phần typeahead mặc định hiển thị 5 kết quả nên khi xây dựng truy vấn, chúng ta có thể giới hạn số lượng kết quả trả về giúp cho truy vấn khi thực hiện sẽ chạy nhanh hơn.
      $customer = Customer::where(‘name’, ‘like’, ‘%’ . $request->get(‘q’) . ‘%’)->take(5)->get();
      3. Ngoài ra chúng ta có thể chỉ thực hiện gợi ý khi người dùng gõ vào vài từ thông qua thiết lập minLength, ví dụ chúng ta nên thiết lập khi gõ từ 3-5 từ mới gợi ý như vậy khi tìm kiếm cũng giới hạn số lượng kết quả dẫn đến truy vấn nhanh hơn.

      1. Tối ưu chuẩn, chạy nhanh hơn hẳn, mở network activity thấy request giảm từ 4.6 giây xuống còn 241ms, quá ngon. Thanks

  3. Ad viết bài rất công phu và chi tiết, dễ hiểu, Mong mọi người biết đến trang web của ad nhiều hơn.

  4. sao e dùng theo như trên mà search toàn ra không có kết quả tìm kiếm nhỉ . cái url: ‘api/customer?q=%QUERY%’, là ntn vậy a

    1. Bạn debug từng bước một, trước tiên xem api hoạt động tốt chưa đã http://typeahead.dev/api/customer?q=jo nếu ok rồi thì kiểm tra kỹ xem đã thiết lập typeahead chuẩn chưa? Với URL api/customer?q=%QUERY% thì QUERY chính là word người dùng nhập vào và nó so sánh matching hai đầu.

Add Comment