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

4,481 阅读15分钟
原文链接: www.zcfy.cc

(相关阅读: node.js, vue.js, Elasticsearch

介绍

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

目录

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

入门

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

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

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

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

要安装这些库,执行:

npm install express body-parser elasticsearch

现在,您的环境的第一部分已经建立。但是,您的设置中缺少Elasticsearch。您将需要安装Elasticsearch。有不同的方法来安装Elasticsearch。如果您使用Debian Linux操作系统,则可以下载.deb文件并使用dpkg进行安装。

//下载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的指南。

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

// 启动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引擎。