Hướng dẫn viết app search realtime bằng NodeJS

Trong hướng dẫn này, chúng ta sẽ học cách xây dựng app search realtime bằng NodeJS. Ứng dụng này sẽ hỗ trợ cho việc tìm kiếm realtime . Giúp tăng trải nghiệm người dùng, và giúp tìm kiếm nhanh chóng hơn.


1055

Tạo Environment NodeJS

Đầu tiên, chúng ta cần tạo enviroment cho project NodeJS. Cách dễ nhất để tạo enviroment là tạo một thư mục mới và chạy npm init. Tạo một thư mục mới gọi là elastic-node,vào thư mục mới và sau đó chạy npm init:

Tạo một thư mục mới với tên là elastic-node:

mkdir elastic-node

Di chuyển vào thư mục mới:

cd elastic-node

Chạy npm init để tạo file package.json:

npm init

Các lệnh trên sẽ đưa bạn giúp tạo file package.json, lưu trữ mọi thư viện của NodeJS

Tiếp theo, bạn cần cài đặt các thư viện cần thiết cho công cụ search real-time. Cài đặt các thư viện bằng lệnh sau:

npm install express body-parser elasticsearch

Thư viện express sẽ chạy server, trong khi thư viện body-parser dùng để phân tích body các request. elaticsearch là thư viện NodeJS chính thức cho Elaticsearch, đây là công cụ search realtime sẽ được xây dựng.

Đối với các bản phân phối / hệ điều hành khác, bạn có thể tìm thấy một hướng dẫn về cách cài đặt Elaticsearch tại đây.

Elaticsearch không start tự động sau khi cài đặt. Elaticsearch có thể được start và stop sử dụng lệnh service .

Chạy các bước sau để start service elaticsearch:

sudo -i service elasticsearch start

Bạn có thể stop service elaticsearch bằng lệnh này:

sudo -i service elasticsearch stop

Để config Elaticsearch tự động start khi hệ thống khởi động, hãy chạy nó để reload daemon systemctl:

sudo /bin/systemctl daemon-reload

Sau đó kích hoạt elaticsearch để có thể được gọi như service:

sudo /bin/systemctl enable elasticsearch.service

Sau khi chạy lệnh ở trên, bạn có thể start Elaticsearch bằng lệnh này:

sudo systemctl start elasticsearch.service

Stop với câu lệnh sau

sudo systemctl stop elasticsearch.service

Bạn cũng có thể check status của Elaticsearch:

sudo service elasticsearch status

Index Data trong Elasticsearch

Tạo file data.js trong thư mục gốc của bạn và thêm code sau trong app NodeJS:

//require the Elasticsearch librray
const elasticsearch = require('elasticsearch');
// instantiate an Elasticsearch client
const client = new elasticsearch.Client({
   hosts: [ 'http://localhost:9200']
});
// ping the client to be sure Elasticsearch is up
client.ping({
     requestTimeout: 30000,
 }, function(error) {
 // at this point, eastic search is down, please check your Elasticsearch service
     if (error) {
         console.error('Elasticsearch cluster is down!');
     } else {
         console.log('Everything is ok');
     }
 });

Đầu tiên, đoạn code này yêu cầu thư viện Elaticsearch và sau đó setup Elasticsearch, http: // localhost: 9200. Vì mặc định, Elaticsearch listen trên: 9200. Tiếp theo, bạn ping client Elaticsearch để chắc chắn rằng máy chủ đang hoạt động. Nếu bạn chạy node data.js, bạn sẽ nhận được message cho biết ‘Everything is ok’.

Hiểu rõ Indexes

Không giống như các cơ sở dữ liệu thông thường, index Elaticsearch là nơi lưu trữ các document liên quan. Ví dụ: bạn sẽ tạo index có tên scotch.io-tutorial để lưu trữ dữ liệu thuộc loại cities_list. Đây là cách nó được thực hiện trong Elaticsearch:

// create a new index called scotch.io-tutorial. If the index has already been created, this function fails safely
client.indices.create({
      index: 'scotch.io-tutorial'
  }, function(error, response, status) {
      if (error) {
          console.log(error);
      } else {
          console.log("created a new index", response);
      }
});

Thêm đoạn code này sau hàm ping bạn đã viết trước đó. Bây giờ, chạy lại node data.js. Bạn sẽ nhận được hai message:

  • Everything is okay
  • Created a new index (với response từ Elasticsearch)

Thêm Document vào Index

Bạn có thể thêm document vào các index có sẵn với API Elaticsearch. Để làm điều này, thêm đoạn code sau:

// add a data to the index that has already been created
client.index({
     index: 'scotch.io-tutorial',
     id: '1',
     type: 'cities_list',
     body: {
         "Key1": "Content for key one",
         "Key2": "Content for key two",
         "key3": "Content for key three",
     }
 }, function(err, resp, status) {
     console.log(resp);
 });

Body muốn thêm document vào index scotch.io-tutorial, trong khi loại này thuộc nhiều danh mục. Tuy nhiên, lưu ý rằng nếu key id bị bỏ qua, Elaticsearch sẽ tự động tạo key.

Trong hướng dẫn này, document của bạn sẽ là danh sách tất cả các thành phố trên thế giới. Nếu bạn muốn thêm từng thành phố một, có thể mất vài ngày để lập index tất cả. May mắn thay, Elaticsearch có chức năng bulk để xử lý dữ liệu số lượng lớn.

Đầu tiên, lấy tệp JSON chứa tất cả các thành phố trên thế giới ở đây và lưu tệp đó vào thư mục gốc của bạn dưới dạng cities.json.

Đã đến lúc sử dụng API bulk để import dữ liệu :

// require the array of cities that was downloaded
const cities = require('./cities.json');
// declare an empty array called bulk
var bulk = [];
//loop through each city and create and push two objects into the array in each loop
//first object sends the index and type you will be saving the data as
//second object is the data you want to index
cities.forEach(city =>{
   bulk.push({index:{
                 _index:"scotch.io-tutorial",
                 _type:"cities_list",
             }
         })
  bulk.push(city)
})
//perform bulk indexing of the data passed
client.bulk({body:bulk}, function( err, response  ){
         if( err ){
             console.log("Failed Bulk operation".red, err)
         } else {
             console.log("Successfully imported %s".green, cities.length);
         }
});

Tại đây, bạn chạy vòng lặp qua tất cả các thành phố trong tệp JSON và tại mỗi vòng lặp, bạn nối thêm một đối tượng vớiindex và type document bạn sẽ lập index.

Sử dụng Express với landing page

Ví dụ Elaticsearch của bạn đang hoạt động và bạn có thể kết nối với nó bằng NodeJS. Đã đến lúc sử dụng Express với landing page và setup chúng

Tạo một tệp có tên là index.js và thêm đoạn code sau:

//require the Elasticsearch librray
const elasticsearch = require('elasticsearch');
// instantiate an elasticsearch client
const client = new elasticsearch.Client({
   hosts: [ 'http://localhost:9200']
});
//require Express
const express = require( 'express' );
// instanciate an instance of express and hold the value in a constant called app
const app     = express();
//require the body-parser library. will be used for parsing body requests
const bodyParser = require('body-parser')
//require the path library
const path    = require( 'path' );
// ping the client to be sure Elasticsearch is up
client.ping({
     requestTimeout: 30000,
 }, function(error) {
 // at this point, eastic search is down, please check your Elasticsearch service
     if (error) {
         console.error('elasticsearch cluster is down!');
     } else {
         console.log('Everything is ok');
     }
 });
// use the bodyparser as a middleware
app.use(bodyParser.json())
// set port for the app to listen on
app.set( 'port', process.env.PORT || 3001 );
// set path to serve static files
app.use( express.static( path.join( __dirname, 'public' )));
// enable CORS
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});
// defined the base route and return with an HTML file called tempate.html
app.get('/', function(req, res){
  res.sendFile('template.html', {
     root: path.join( __dirname, 'views' )
   });
})
// define the /search route that should return elastic search results
app.get('/search', function (req, res){
  // declare the query object to search elastic search and return only 200 results from the first result found.
  // also match any data where the name is like the query string sent in
  let body = {
    size: 200,
    from: 0,
    query: {
      match: {
          name: req.query['q']
      }
    }
  }
  // perform the actual search passing in the index, the search query and the type
  client.search({index:'scotch.io-tutorial',  body:body, type:'cities_list'})
  .then(results => {
    res.send(results.hits.hits);
  })
  .catch(err=>{
    console.log(err)
    res.send([]);
  });
})
// listen on the specified port
app .listen( app.get( 'port' ), function(){
  console.log( 'Express server listening on port ' + app.get( 'port' ));
} );

Nhìn vào đoạn mã trên, bạn sẽ thấy đoạn code đã thực hiện như sau:

  • Yêu cầu các thư viện Express, body-Parser và path.
  • Set a new instance of Express to the constant called app.
  • Set phiên bản mới của Express thành ứng dụng được gọi là hằng số.
  • Set the app to use the bodyParser middleware.
  • Set app sử dụng bodyParser, middleware.
  • Set static của app thành thư mục có tên public. Thư mục này chưa được tạo.
  • Define middleware có thêm header CORS cho app.
  • Define route GET cho URL root của app, được biểu thị bằng /. Trong route này, code trả về một file có tên template.html nằm trong thư mục views.
  • Define route  GET cho URL /search của app. Truy vấn search chính gồm đối tượng truy vấn. Bạn có thể thêm các truy vấn tìm kiếm khác nhau cho đối tượng này. Đối với truy vấn này, bạn thêm key với truy vấn và trả về một đối tượng cho nó biết rằng tên của document bạn đang tìm phải khớp với req.query [‘q’].

Hiểu rõ Response API Search

Nếu bạn xem log response từ API search, nó sẽ bao gồm rất nhiều thông tin. Dưới đây là một ví dụ:

Output{ took: 88,
timed_out: false,
_shards: { total: 5, successful: 5, failed: 0 },
hits:
{ total: 59,
 max_score: 5.9437823,
 hits:
  [ {"_index":"scotch.io-tutorial",
  "_type":"cities_list",
  "_id":"AV-xjywQx9urn0C4pSPv",
  "_score":5.9437823,"
  _source":{"country":"ES","name":"A Coruña","lat":"43.37135","lng":"-8.396"}},
    [Object],
...
    [Object] ] } }

Response bao gồm một thuộc tính took cho số mili giây để tìm kết quả. timed_out sẽ true nếu không tìm thấy kết quả nào trong thời gian tối đa được phép. _shards cho biết thông tin về status của các node khác nhau (nếu được deploy dưới dạng cluster của node), và hits, bao gồm các kết quả tìm kiếm.

Trong thuộc tính hits , bạn có một đối tượng với các thuộc tính sau:

  • total hiển thị tổng số mục phù hợp
  • max_score là số điểm tối đa của các mục được tìm thấy.
  • hits là một mảng bao gồm các mục tìm thấy.

Đây là lý do tại sao bạn return response.hits trong route search, nơi chứa các document được tìm thấy.

Tạo Template HTML

Đầu tiên, tạo hai thư mục mới trong thư mục gốc có tên là views và public được tham chiếu ở bước trước. Tiếp theo, tạo file có tên template.html trong thư mục views  và thêm đoạn code sau:

<!-- template.html -->
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div class="container" id="app">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <h1>Search Cities around the world</h1>
        </div>
    </div>
    <div class="row">
        <div class="col-md-4 col-md-offset-3">
            <form action="" class="search-form">
                <div class="form-group has-feedback">
                    <label for="search" class="sr-only">Search</label>
                    <input type="text" class="form-control" name="search" id="search" placeholder="search" v-model="query" >
                    <span class="glyphicon glyphicon-search form-control-feedback"></span>
                </div>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-3" v-for="result in results">
            <div class="panel panel-default">
                <div class="panel-heading">
                <!-- display the city name and country  -->
                    {{ result._source.name }}, {{ result._source.country }}
                </div>
                <div class="panel-body">
                <!-- display the latitude and longitude of the city  -->
                    <p>lat:{{ result._source.lat }}, long: {{ result._source.lng }}.</p>
                </div>
            </div>
        </div>
    </div>
</div>
<!--- some styling for the page -->
<style>
    .search-form .form-group {
        float: right !important;
        transition: all 0.35s, border-radius 0s;
        width: 32px;
        height: 32px;
        background-color: #fff;
        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
        border-radius: 25px;
        border: 1px solid #ccc;
    }
    .search-form .form-group input.form-control {
        padding-right: 20px;
        border: 0 none;
        background: transparent;
        box-shadow: none;
        display: block;
    }
    .search-form .form-group input.form-control::-webkit-input-placeholder {
        display: none;
    }
    .search-form .form-group input.form-control:-moz-placeholder {
        /* Firefox 18- */
        display: none;
    }
    .search-form .form-group input.form-control::-moz-placeholder {
        /* Firefox 19+ */
        display: none;
    }
    .search-form .form-group input.form-control:-ms-input-placeholder {
        display: none;
    }
    .search-form .form-group:hover,
    .search-form .form-group.hover {
        width: 100%;
        border-radius: 4px 25px 25px 4px;
    }
    .search-form .form-group span.form-control-feedback {
        position: absolute;
        top: -1px;
        right: -2px;
        z-index: 2;
        display: block;
        width: 34px;
        height: 34px;
        line-height: 34px;
        text-align: center;
        color: #3596e0;
        left: initial;
        font-size: 14px;
    }
</style>

Trong đoạn code trên, có hai phần chính: HTML và CSS.

Trong phần HTML, bạn cần ba thư viện khác nhau:

  1. Bootstrap CSS để tạo style cho trang
  2. Axios js để thực hiện các yêu cầu HTTP đến máy chủ
  3. Vue.js đó là một framework đơn giản mà bạn sẽ sử dụng.

Trong phần CSS, bạn đã tạo style ẩn cho input tìm kiếm và hiển thị chính nó khi bạn rê chuột qua icon(biểu tượng) tìm kiếm.

Tiếp theo, có một input cho box search mà bạn đã gán v-model của nó cho truy vấn. Sau này, bạn dùng vòng lặp qua tất cả các kết quả.

Chạy lệnh node index.js và sau đó truy cập http://localhost: 3001/ trong trình duyệt. Kết quả như sau

Search app landing page

Tiếp theo, thêm tag script vào tệp template.html của bạn:

// create a new Vue instance
var app = new Vue({
    el: '#app',
    // declare the data for the component (An array that houses the results and a query that holds the current search string)
    data: {
        results: [],
        query: ''
    },
    // declare methods in this Vue component. here only one method which performs the search is defined
    methods: {
        // make an axios request to the server with the current search query
        search: function() {
            axios.get("http://127.0.0.1:3001/search?q=" + this.query)
                .then(response => {
                    this.results = response.data;
                })
        }
    },
    // declare Vue watchers
    watch: {
        // watch for change in the query string and recall the search method
        query: function() {
            this.search();
        }
    }
})

Trong phần này, bạn đã khai báo Vue, gắn nó vào phần tử với id của ứng dụng. Bạn đã khai báo các thuộc tính dữ liệu bao gồm truy vấn, mà bạn đã đính kèm với input search và results, là một mảng của tất cả các kết quả được tìm thấy.

Trong methods quota , bạn chỉ có một hàm gọi là search, sẽ kích hoạt request GET cho route search . Điều này đựa theo input hiện tại trong box search. Đến lượt nó returns response được lặp trong HTML.

Cuối cùng, bạn sử dụng watchers trong Vue.js, thực hiện một hành động bất cứ lúc nào để xem dữ liệu thay đổi. Tại đây, bạn đang theo dõi sự thay đổi trong dữ liệu truy vấn và khi thay đổi, phương thức search sẽ được kích hoạt.

Nếu bạn chạy lại lệnh index.js ngay bây giờ và điều hướng đến http: // localhost: 3001 / lần nữa, nó sẽ hoạt động như sau:
Completed search app that displays results

Search từ Client

Nếu bạn không muốn gửi yêu cầu đến server mỗi khi tìm kiếm xảy ra, bạn có thể tìm kiếm công cụ Elaticsearch từ phía client. Một số developer có thể không thoải mái với việc truy cập server của họ cho mọi cụm từ tìm kiếm, trong khi một số developer cảm thấy an toàn hơn khi tìm kiếm từ phía server.

Elaticsearch cung cấp một bản build trình duyệt có thể search. Bước này sẽ hướng dẫn bạn search từ browser client.

Đầu tiên, thêm route mới vào file Express và restart máy chủ:

// decare a new route. This route serves a static HTML template called template2.html
app.get('/v2', function(req, res){
  res.sendFile('template2.html', {
     root: path.join( __dirname, 'views' )
   });
})

Trong đoạn code này, bạn đã tạo route mới cho URL tại / v2 để trả về fileHTML tĩnh có tên là template2.html. Bạn sẽ tạo tập tin này ở bước sau.

Tiếp theo, bạn cần tải thư viện Elaticsearch tại đây. Sau khi tải xong, giải nén và sao chép elasticsearch.min.js vào thư mục public trong thư mục gốc của app.


http.cors.enabled : true
http.cors.allow-origin : "*"

Sau khi xong, hãy khởi động lại Elaticsearch:

sudo service elasticsearch restart

Tiếp theo, tạo file có tên template2.html trong thư mục views của bạn và thêm code sau:

<!-- template2.html -->
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div class="container" id="app">
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <h1>Search Cities around the world</h1>
        </div>
    </div>
    <div class="row">
        <div class="col-md-4 col-md-offset-3">
            <form action="" class="search-form">
                <div class="form-group has-feedback">
                    <label for="search" class="sr-only">Search</label>
                    <input type="text" class="form-control" name="search" id="search" placeholder="search" v-model="query" >
                    <span class="glyphicon glyphicon-search form-control-feedback"></span>
                </div>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-3" v-for="result in results">
            <div class="panel panel-default">
                <div class="panel-heading">
                <!-- display the city name and country  -->
                    {{ result._source.name }}, {{ result._source.country }}
                </div>
                <div class="panel-body">
                <!-- display the latitude and longitude of the city  -->
                    <p>lat:{{ result._source.lat }}, long: {{ result._source.lng }}.</p>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="/elasticsearch.min.js"></script>
<style>
    .search-form .form-group {
        float: right !important;
        transition: all 0.35s, border-radius 0s;
        width: 32px;
        height: 32px;
        background-color: #fff;
        box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset;
        border-radius: 25px;
        border: 1px solid #ccc;
    }
    .search-form .form-group input.form-control {
        padding-right: 20px;
        border: 0 none;
        background: transparent;
        box-shadow: none;
        display: block;
    }
    .search-form .form-group input.form-control::-webkit-input-placeholder {
        display: none;
    }
    .search-form .form-group input.form-control:-moz-placeholder {
        /* Firefox 18- */
        display: none;
    }
    .search-form .form-group input.form-control::-moz-placeholder {
        /* Firefox 19+ */
        display: none;
    }
    .search-form .form-group input.form-control:-ms-input-placeholder {
        display: none;
    }
    .search-form .form-group:hover,
    .search-form .form-group.hover {
        width: 100%;
        border-radius: 4px 25px 25px 4px;
    }
    .search-form .form-group span.form-control-feedback {
        position: absolute;
        top: -1px;
        right: -2px;
        z-index: 2;
        display: block;
        width: 34px;
        height: 34px;
        line-height: 34px;
        text-align: center;
        color: #3596e0;
        left: initial;
        font-size: 14px;
    }
</style>

Tiếp theo, thêm tag script vào file template2.html của bạn và thêm code sau:

// instantiate a new Elasticsearch client like you did on the client
var client = new elasticsearch.Client({
    hosts: ['http://127.0.0.1:9200']
});
// create a new Vue instance
var app = new Vue({
    el: '#app',
    // declare the data for the component (An array that houses the results and a query that holds the current search string)
    data: {
        results: [],
        query: ''
    },
    // declare methods in this Vue component. here only one method which performs the search is defined
    methods: {
        // function that calls the elastic search. here the query object is set just as that of the server.
        //Here the query string is passed directly from Vue
        search: function() {
            var body = {
                    size: 200,
                    from: 0,
                    query: {
                        match: {
                            name: this.query
                        }
                    }
                }
                // search the Elasticsearch passing in the index, query object and type
            client.search({ index: 'scotch.io-tutorial', body: body, type: 'cities_list' })
                .then(results => {
                    console.log(`found ${results.hits.total} items in ${results.took}ms`);
                    // set the results to the result array we have
                    this.results = results.hits.hits;
                })
                .catch(err => {
                    console.log(err)
                });
        }
    },
    // declare Vue watchers
    watch: {
        // watch for change in the query string and recall the search method
        query: function() {
            this.search();
        }
    }
})

Đoạn code HTML và JavaScript ở trên tương tự như đoạn ở bước trước, với các điểm khác biệt chính như sau:

  • Bạn không require  Axios, thay vào đó bạn required  elaticsearch.js.
  • Ở đầu thẻ script, bạn đã khởi tạo ứng dụng clientElaticsearch như được thực hiện ở phía server-side.
  • Phương thức search không thực hiện yêu cầu HTTP, mà là search công cụ Elaticsearch như được thực hiện trong routesearch ở phía server.

Nếu bạn vào url http: // localhost: 3001 / v2, nó sẽ hoạt động như được sau:
Client side searching from the search app

Kết luận

Trong hướng dẫn này, bạn đã sử dụng Elaticsearch để lập chỉ mục dữ liệu. Bạn cũng đã triển khai search realtime với NodeJS bằng thư viện Elaticsearch.

Tham khảo thêm về NodeJS : Crawl dữ liệu website bằng NodeJS với Scotch và Express


Like it? Share with your friends!

1055