使用Node,Vue和ElasticSearch構建實時搜尋引擎

_小生_發表於2019-02-16

(譯者注:相關閱讀: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引擎。

原文閱讀:https://scotch.io/tutorials/b…

相關文章