使用Node,Vue和ElasticSearch构建实时搜索引擎

(译者注:相关阅读:node.js,vue.js,Elasticsearchcss

介绍

Elasticsearch是一个分布式的RESTful搜索和分析引擎,可以解决愈来愈多的用例。 Elasticsearch创建在Apache Lucene之上,它是一个高性能的文本搜索引擎库。html

目录

在今天的课程中,您将学习如何使用Node.js,Elasticsearch和Vue.js构建实时搜索引擎。所以,须要对本教程进行基本的Vue.js和Node.js(Express)理解。vue

入门

让咱们开始为本课设置环境。因为您将使用Node.js,所以最简单的入门方法是建立一个新文件夹并运行npm init。建立一个名为elastic-node的新文件夹,将目录更改成新文件夹,而后运行npm init:node

//建立一个名为elastic-node的新目录
mkdir elastic-node
//将目录更改成建立的新文件夹
cd elastic-node
//运行npm init来建立一个package.json文件
npm init

上述命令将引导您完成建立package.json文件的过程,该文件是运行任何Node.js库所必需的。接下来,您须要安装实时搜索引擎所需的库。所需的库是:ios

  • Express: 这个库将运行咱们的服务器
  • Body-parser: 该库与Express一块儿使用来分析正文请求。
  • Elasticsearch: 这是Elasticsearch的官方Node.js库,它是实时搜索的引擎。

要安装这些库,执行:git

npm install express body-parser elasticsearch

如今,您的环境的第一部分已经创建。可是,您的设置中缺乏Elasticsearch。您将须要安装Elasticsearch。有不一样的方法来安装Elasticsearch。若是您使用Debian Linux操做系统,则能够下载.deb文件并使用dpkg进行安装。github

//下载deb包
curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.4.deb
//使用dpkg安装deb包
sudo dpkg -i elasticsearch-5.6.4.deb

对于其余发行版/操做系统,您能够在 这里找到关于如何安装Elasticsearch的指南。web

Elasticsearch安装后不会自动启动。 Elasticsearch可使用服务命令启动和中止:chrome

// 启动Elasticsearch服务
sudo -i service elasticsearch start
// 中止Elasticsearch服务
sudo -i service elasticsearch stop

要将Elasticsearch配置为在系统启动时自动启动,请运行:数据库

// 从新加载systemctl守护进程
sudo /bin/systemctl daemon-reload
// enable elastic search so it can be called as a service
sudo /bin/systemctl enable elasticsearch.service

运行上面的命令后,您能够运行如下命令来启动和中止Elasticsearch:

// 启动Elasticsearch服务
sudo systemctl start elasticsearch.service
// 中止Elasticsearch服务
sudo systemctl stop elasticsearch.service

检查Elasticsearch的状态:

// Elasticsearch的状态
sudo service elasticsearch status
注意: Google Chrome Elastic工具箱能够帮助您快速查看Elasticsearch的索引和文档。

在Elasticsearch中索引数据

在根文件夹中建立一个data.js文件并添加:

//data.js
//require the Elasticsearch librray
const elasticsearch = require('elasticsearch');
// 实例化一个Elasticsearch客户端
const client = new elasticsearch.Client({
   hosts: [ 'http://localhost:9200']
});
// ping客户端以确保Elasticsearch已启动
client.ping({
     requestTimeout: 30000,
 }, function(error) {
 // 此时,eastic搜索已关闭,请检查您的Elasticsearch服务
     if (error) {
         console.error('Elasticsearch cluster is down!');
     } else {
         console.log('Everything is ok');
     }
 });

让我来解释一下你在上面的代码块中所作的事情:首先,你须要Elasticsearch库,并创建一个新的Elasticsearch客户端传入一个主机的数组。若是您注意到,主机是http:// localhost:9200。这是由于默认状况下,Elasticsearch在端口9200上监听。接下来,您ping Elasticsearch客户端以确保服务器已启动。若是你运行节点data.js,你应该获得一个消息说一切正常。

了解索引

与普通数据库不一样,Elasticsearch索引是存储相关文档的地方。例如,您将建立一个名为scotch.io-tutorial的索引来存储类型为cities_list的数据。这就是Elasticsearch所作的工做:

// data.js
// 建立一个名为scotch.io-tutorial的新索引。若是索引已经被建立,这个函数会安全地失败
client.indices.create({
      index: 'scotch.io-tutorial'
  }, function(error, response, status) {
      if (error) {
          console.log(error);
      } else {
          console.log("created a new index", response);
      }
});

在以前编写的ping功能以后添加这段代码。如今再次运行node data.js,你应该获得两条消息:

  • Everything is okay(一切正常)
  • Created a new index (with the response from Elasticsearch)(建立了一个新的索引(来自Elasticsearch的响应) )

将文档添加到索引

Elasticsearch API使文档能够轻松添加到已建立的索引中。以下:

// 将数据添加到已建立的索引
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);
 });

上面的代码块是解释性的。正文指的是您要添加到scotch.io-tutorial索引的文档,而类型更多的是一个类别。可是,请注意,若是id键被省略,Elasticsearch将自动生成一个。

可是,在本课中,您的文档将成为世界上全部城市的列表。若是您要逐个添加每一个城市,那么须要几天时间(若是不是几周)才能彻底索引全部城市。幸运的是,Elasticsearch有一个用于处理批量数据的批量函数。

首先,抓取包含世界上全部城市的JSON文件,并保存到您的根文件夹中做为cities.json

如今是时候使用批量API来导入咱们大量数据了:

//data.js
// require the array of cities that was downloaded
const cities = require('./cities.json');
// 声明一个名为bulk的空数组
var bulk = [];
// 循环遍历每一个城市,并在每一个循环中建立并将两个对象推入数组中
// 第一个对象发送索引和类型,保存数据
// 第二个对象是你想索引的数据
cities.forEach(city =>{
   bulk.push({index:{ 
                 _index:"scotch.io-tutorial", 
                 _type:"cities_list",
             }          
         })
  bulk.push(city)
})
// 对传递的数据执行批量索引
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); 
         } 
});

在这里,您已经浏览了JSON文件中的全部城市,而且在每一个循环中,您都会追加一个包含要索引的文档的索引和类型的对象。请注意,在循环中有两个推入数组?这是由于批量API须要首先包含索引定义的对象,而后是要索引的文档。欲了解更多信息,你能够在这里查看

接下来,您将传递给新的批量数组的client.bulk函数做为正文调用。这会将全部数据用scotch.io-tutorial的索引和类型cities_list索引到Elasticsearch中。

引入Express

您的Elasticsearch实例已启动并正在运行,您可使用Node.js链接它。如今是时候使用Express来为目标页面提供服务,并使用迄今为止运行的设置。

建立一个名为index.js的文件并添加:

//index.js
// 须要Elasticsearch librray
const elasticsearch = require('elasticsearch');
// 实例化一个elasticsearch客户端
const client = new elasticsearch.Client({
   hosts: [ 'http://localhost:9200']
});
//require Express
const express = require( 'express' );
// 实例化一个表达式的实例并将其保存在一个名为app的常量中
const app     = express();
// 引入body-parser库。将用于解析主体请求
const bodyParser = require('body-parser')
//require the path library
const path    = require( 'path' );

// ping客户端以确保Elasticsearch已启动
client.ping({
     requestTimeout: 30000,
 }, function(error) {
 // 此时,eastic搜索已关闭,请检查您的Elasticsearch服务
     if (error) {
         console.error('elasticsearch cluster is down!');
     } else {
         console.log('Everything is ok');
     }
 });

// 使用bodyparser做为中间件
app.use(bodyParser.json())
// 设置应用程序侦听的端口
app.set( 'port', process.env.PORT || 3001 );
// 设置路径来提供静态文件
app.use( express.static( path.join( __dirname, 'public' )));
// 启用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();
});

// 定义了基本路线并返回一个名为tempate.html的HTML文件
app.get('/', function(req, res){
  res.sendFile('template.html', {
     root: path.join( __dirname, 'views' )
   });
})

// 定义应该返回弹性搜索结果的/ search路径
app.get('/search', function (req, res){
  // 声明查询对象以搜索弹性搜索,并从找到的第一个结果中仅返回200个结果。
  // 还匹配其中名称与发送的查询字符串相似的任何数据
  let body = {
    size: 200,
    from: 0, 
    query: {
      match: {
          name: req.query['q']
      }
    }
  }
  // 在索引中执行实际的搜索传递,搜索查询和类型
  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([]);
  });

})
// 监听一个指定的端口
app .listen( app.get( 'port' ), function(){
  console.log( 'Express server listening on port ' + app.get( 'port' ));
} );

看看上面的代码,注意:

  • 须要Express,body-parser和路径库。
  • 将一个新的Express实例设置为常量,命名为app。
  • 设置应用程序以使用bodyParser中间件。
  • 将应用程序的静态文件放在名为public的文件夹(我还没有建立此文件夹)。
  • 定义了一个将CORS头添加到应用程序的中间件。
  • 定义一个GET路由在根目录文件夹里,而且在此路由中,我返回了一个名为template.html的文件,该文件位于views文件夹中(我还还没有建立此文件夹和文件template.html)
  • 为应用程序的/ search URL定义了一个GET路由,该路径使用查询对象来搜索经过查询字符串传递给它的数据的匹配。主要的搜索查询包含在查询对象中。您能够向此对象添加不一样的搜索查询。对于这个查询,你在查询中添加一个关键字并返回一个对象,告诉它你正在查找的文档的名字应该与req.query ['q']匹配。

    Besides the query object, the search body can contain other optional properties, including size and from. The size property determines the number of documents to be included in the response. If this value is not present, by default ten documents are returned. The from property determines the starting index of the returned documents. This is useful for pagination.

了解搜索API响应

若是您要注销搜索API的响应,则会包含大量信息。

{ 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] ] } }

响应中包含一个用于查找结果的毫秒数的夺取属性timed_out,若是在最大容许时间内未找到结果,则返回true; _shards用于获取有关不一样节点状态的信息(若是部署为节点集群)以及包含搜索结果的匹配。

在hits属性中,咱们有一个对象具备如下属性:

总数显示匹配项目的总数。

max_score是找到的项目的最高分数。

命中包含找到的项目的数组。

以上是搜索路由的前提,您返回了response.hits.hits,其中包含找到的文档。

建立HTML模板

首先,在上面的部分中引用的名为views和public的根文件夹中建立两个新文件夹。接下来,在views文件夹中建立一个名为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>
    <div>
        <div>
            <h1>Search Cities around the world</h1>
        </div>
    </div>
    <div>
        <div>
            <form action="" class="search-form">
                <div>
                    <label for="search" class="sr-only">Search</label>
                    <input type="text" class="form-control" name="search" id="search" placeholder="search" v-model="query" >
                    <span></span>
                </div>
            </form>
        </div>
    </div>
    <div>
        <div>
            <div>
                <div>
                
                    {{ result._source.name }}, {{ result._source.country }} 
                </div>
                <div>
                
                    <p>lat:{{ result._source.lat }}, long: {{ result._source.lng }}.</p>
                </div>
            </div>
        </div>
    </div>
</div>

<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>

在上面的代码片断中,有两个主要部分:

  • HTML代码:在本节中,您首先须要三个不一样的库,分别是1.)Bootstrap CSS,用于设置页面样式。 2.)Axios js,用于向咱们的服务器发送HTTP请求,以及3)Vue.js,一个您将用于咱们的视图的简约框架。
  • CSS代码:在这里,您将悬停在搜索图标上的样式应用于隐藏和显示搜索输入。

接下来,为您指定其v模型进行查询的搜索框有一个输入(这将由Vue.js使用)。在此以后,您循环遍历全部结果(此循环和结果变量将由Vue.js提供)。请注意,在此循环时,您必须访问数据的__source属性。基于弹性搜索返回的响应,这看起来很熟悉。

运行node index.js命令,浏览到http:// localhost:3001 /,接下来,在你的template.html文件中添加一个脚本标签,添加:

// template.html
// 建立一个新的Vue实例
var app = new Vue({
    el: '#app',
    // 声明组件的数据(容纳结果的数组以及包含当前搜索字符串的查询) search string)
    data: {
        results: [],
        query: ''
    },
    // 在这个Vue组件中声明方法。这里只定义了一种执行搜索的方法
    methods: {
        // 使用当前搜索查询向服务器发出axios请求
        search: function() {
            axios.get("http://127.0.0.1:3001/search?q=" + this.query)
                .then(response => {
                    this.results = response.data;

                })
        }
    },
    // declare Vue watchers
    watch: {
        // 注意查询字符串中的更改并调用搜索方法
        query: function() {
            this.search();
        }
    }

})

Vue.js代码:在本节中,您声明了一个Vue的新实例,将其挂载到具备应用程序ID的元素上。您声明了数据属性,其中包括1)查询您已附加到搜索输入,和2)结果,这是全部找到的结果的数组。

在方法配置中,只有一个称为搜索的函数,它会触发搜索路径的GET请求,以传递搜索框中的当前输入。而后会返回一个响应,而后在HTML代码块中循环。

最后,您使用Vue.js中的所谓观察者,在任什么时候候均可以监视数据以查看更改。在这里,您正在观察查询数据中的更改,而且一旦它发生更改,就会触发搜索方法。

从客户端搜索

每次搜索发生时,若是我不想将请求发送到服务器,该怎么办?我能够直接从客户端搜索Elasticsearch引擎吗?是。

尽管上述方法有效,但有些开发人员可能并不习惯于每次搜索条件都使用他们的服务器,有些则认为从服务器端进行搜索更安全。

可是,能够从客户端进行搜索。 Elasticsearch提供了能够进行搜索的浏览器版本。让我经过一个快速示例。

首先,将一条新路线添加到Express文件并从新启动服务器:

//index.js
// 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' )
   });
})

在上面的代码块中,您为/ v2建立了一个新的URL路由,而且您在此路由中所作的全部操做都将返回一个名为template2.html的静态HTML文件,该文件将很快建立。

接下来,您须要在这里下载Elasticsearch的客户端库。下载后,将elasticsearch.min.js提取并复制到应用程序根目录中的公用文件夹。

注意:了解您是否尝试从客户端链接Elasticsearch引擎很是重要,您可能会遇到CORS问题。为了解决这个问题,找到你的Elasticsearch配置文件(对于Debian / Ubuntu,能够在/etc/elasticsearch/elasticsearch.yml找到它)。对于其余操做系统,找到它位于的位置,并将如下内容添加到底部文件:
#/etc/elasticsearch/elasticsearch.yml

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

完成以后,从新启动Elasticsearch实例

// 从新启动Elasticsearch服务
sudo service elasticsearch restart

接下来,在视图文件夹中建立一个名为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>
    <div>
        <div>
            <h1>Search Cities around the world</h1>
        </div>
    </div>
    <div>
        <div>
            <form action="" class="search-form">
                <div>
                    <label for="search" class="sr-only">Search</label>
                    <input type="text" class="form-control" name="search" id="search" placeholder="search" v-model="query" >
                    <span></span>
                </div>
            </form>
        </div>
    </div>
    <div>
        <div>
            <div>
                <div>
                
                    {{ result._source.name }}, {{ result._source.country }} 
                </div>
                <div>
                
                    <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>

接下来,在您的template2.html文件中添加一个脚本标记并添加:

//template2.html
// 像你在客户端上那样实例化一个新的Elasticsearch客户端
var client = new elasticsearch.Client({
    hosts: ['http://127.0.0.1:9200']
});
// 建立一个新的Vue实例
var app = new Vue({
    el: '#app',
    // 声明组件的数据(容纳结果的数组以及包含当前搜索字符串的查询)
    data: {
        results: [],
        query: ''
    },
    // 在这个Vue组件中声明方法。这里只定义了一种执行搜索的方法
    methods: {
        // 函数调用弹性搜索。这里查询对象与服务器的设置同样。
        // 这里查询字符串直接从Vue传递
        search: function() {
            var body = {
                    size: 200,
                    from: 0,
                    query: {
                        match: {
                            name: this.query
                        }
                    }
                }
                // 搜索传入索引的Elasticsearch,查询对象和类型
            client.search({ index: 'scotch.io-tutorial', body: body, type: 'cities_list' })
                .then(results => {
                    console.log(found ${results.hits.total} items in ${results.took}ms);
                    // 将结果设置为咱们拥有的结果数组
                    this.results = results.hits.hits;
                })
                .catch(err => {
                    console.log(err)

                });

        }
    },
    // declare Vue watchers
    watch: {
        // 注意查询字符串中的更改并调用搜索方法
        query: function() {
            this.search();
        }
    }

})

上面的HTML和JavaScript片断与上面的部分很是类似。惟一的区别是:

  • 您不须要Axios,而是须要elasticsearch.js。
  • 在脚本标记的顶部,您启动了Elasticsearch客户端,由于它在服务器端完成。
  • 搜索方法不执行HTTP请求,而是像在服务器端的搜索路径中那样搜索Elasticsearch引擎。

原文阅读:https://scotch.io/tutorials/b...

相关文章
相关标签/搜索