使用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...

相關文章
相關標籤/搜索