احتمالاً تا به حال برای تان پیش آمده که سایتی را جستجو کرده و مدت زمان زیادی صبر کرده اید تا نتایج ظاهر شوند. آیا شما به عنوان یک توسعه دهنده تمایل دارید یک موتور جستجوی بلادرنگ برای سایت تان ایجاد کنید؟
در این مقاله قصد داریم یک موتور جستجوی بلادرنگ (همزمان) برای سایت مان ایجاد کنیم تا کاربران پس از جستجو در سایت، بدون رفرش شدن صفحه بتوانند در سریعترین زمان ممکن به نتایج دسترسی داشته باشند.
ElasticSearch یک موتور جستجوی Restful تحلیلی و توزیع شده است که در موارد زیادی قابل استفاده می باشد. ElasticSearch بر اساس Apache Lucene که یک کتابخانه موتور جستجوی متنی با عملکرد بالا است، ساخته شده است.
در این مقاله به شما یاد می دهیم که یک موتور جستجوی بلادرنگ با ElasticSearch ، Node.js، Vue.js بسازید. برای دنبال کردن این آموزش باید کمی با مباحث پایه ای Node.js و Vue.js آشنایی داشته باشید.
چنانچه تمایل دارید نود جی اس را به صورت مقدماتی تا پیشرفته و پروژه محور یاد بگیرید لطفا روی اینجا کلیک کنید.
علاوه بر این برای آموزش صفر تا صد ویو جی اس نیز می توانید این لینک را دنبال کنید.
ابتدا باید محیطی که برای این پروژه نیاز داریم را آماده کنیم. چون ما از Node.js استفاده می کنیم، ساده ترین راه این است که یک فولدر جدید ایجاد کرده و دستور npm init را اجرا کنیم.
یک فولدر جدید با نام elastic-code ایجاد کرده و سپس وارد این فولدر شده و دستور npm init را در ترمینال اجرا کنید.
//create a new directory called elastic-node mkdir elastic-node //change directory to the new folder created cd elastic-node //run npm init to create a package.json file npm init
دستور بالا یک فایل با نام package.json که برای همه کتابخانه های Node.js مورد نیاز است، ایجاد می کند. سپس باید کتابخانه هایی که برای ساخت موتور جستجوی بلادرنگ مورد نیاز است را نصب می کنیم. این کتابخانه ها عبارتند از:
برای نصب این کتابخانه ها دستور زیر را اجرا کنید.
npm install express body-parser elasticsearch
حال اولین بخش از محیط کارمان را تنظیم کردیم.
در مرحله بعد باید خود ElasticSearch را هم نصب کنیم. روش های مختلفی برای نصب EasticSearch وجود دارد. در صورتی که از سیستم عامل لینوکس دبیان استفاده میکنی، می توانید فایل .deb را دانلود کرده و با استفاده از دستور dpkg آنرا نصب کنید.
//download the deb package curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.6.4.deb //install the deb package using dpkg sudo dpkg -i elasticsearch-5.6.4.deb
برای نصب روی سایر سیستم عامل ها، به مستندات رسمی ElasticSearch استفاده کنید.
ElasticSearch بعد از نصب به طور خودکار راه اندازی نمی شود و تنها با دستورات زیر راه اندازی و متوقف می شود.
// start the Elasticsearch service sudo -i service elasticsearch start // stop the Elasticsearch service sudo -i service elasticsearch stop
اگر می خواهید ElasticSearch را طوری پیکربندی کنید که به محض بالا آمدن سیستم ، راه اندازی شود، دستور زیر را در ترمینال وارد کنید.
//reload the systemctl daemon sudo /bin/systemctl daemon-reload // enable elastic search so it can be called as a service sudo /bin/systemctl enable elasticsearch.service
بعد از اجرای دستورات بالا، می توانید ElasticSearch را با اجرای دستورات زیر راه اندزای (start) کنید و متوقف کنید.
// start the Elasticsearch service sudo systemctl start elasticsearch.service // stop the Elasticsearch service sudo systemctl stop elasticsearch.service
برای بررسی وضعیت ElasticSearch از دستورات زیر استفاده کنید.
// check status of Elasticsearch sudo service elasticsearch status
نکته:یکی از ابزارهای عالی برای کار کردن با ElasticSearch افزونه Elastic toolbox گوگل کروم است.این برنامه به شما کمک می کند تا به سرعت ایندکس ها و مستندات تان را مشاهده کنید.
یک فایل با نام data.js در روت پروژه ایجاد کرده و کدهای زیر را در آن قرار دهید.
//data.js //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'); } });
در بالا، ابتدا کتابخانه ElasticSearch را وارد برنامه کرده و سپس یک کلاینت جدید ElasticSearch ایجاد می کنیم و یک آرایه که شامل یک host است را به آن پاس می دهیم.
اگر دقت کنید مقدار host برابر localhost:9200 است. این بدان خاطر است که بطور پیش فرض ElasticSearch روی پورت 9200 کار میکند. سپس یک ping به کلاینت ElasticSearch ارسال کنید تا مطمئن شوید که سرور در حال کار است. اگر دستور node data.js را اجرا کنید، باید یک پیام با عنوان “Every thing is ok” دریافت کنید.
بر خلاف پایگاه داده های رایج، ایندکس ElasticSearch یک محل برای ذخیره اسناد مرتبط است. برای مثال، شما می توانید یک ایندکس به نام roxo.ir-tutorials برای ذخیره داده های از نوع cities-list ایجاد کنید.
//data.js // create a new index called scotch.io-tutorial. If the index has already been created, this function fails safely client.indices.create({ index: 'roxo.ir-tutorials' }, function(error, response, status) { if (error) { console.log(error); } else { console.log("created a new index", response); } });
تکه کد بالا را بعد از متد ping که در مرحله قبل نوشته اید، اضافه کنید.
حال دوباره دستور node data.js را اجرا کنید.
با اینکار باید دو پیام زیر را دریافت کنید:
توسط Api های ElasticSearch به راحتی می توانید اسناد را به ایندکس هایی که قبلاً ایجاد کرده اید، اضافه کنید. مطابق زیر:
// add a data to the index that has already been created client.index({ index: 'roxo.ir-tutorials', 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 همان سندی است که می خواهیم به ایندکس roxo.ir-tutorials اضافه کنیم. به خاطر داشته باشید که اگر در بالا به کلید id مقداری نسبت ندهید، ElasticSearch یک مقدار به صورت خودکار برای آن تولید می کند.
در این آموزش، سندمان شامل لیستی از تمام شهرهای جهان است. اگر بخواهید هر شهر را یکی یکی وارد کنید، ممکن است روزها یا هفته ها طول بکشد تا تمام آنها را ایندکس کنید. خوشبختانه ElasticSearch یک متد به نام bulk دارد که برای کار با حجم زیاد اطلاعات استفاده می شود.
ابتدا فایل json که تمام شهرهای جهان در آن قرار گرفته را از اینجا دانلود کرده و از حالت فشرده خارج کنید، سپس فایل cities.json را در روت پروژه ذخیره کنید.
در مرحله بعد با استفاده از Api ای که برای ورود دسته جمعی اطلاعات است (bulk) تمام آن اطلاعات را وارد ایندکس می کنیم.
//data.js // 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); } });
در بالا با یک حلقه تکرار تمام شهرها را از فایل json گرفته و آنها را به ایندکس افزوده و یک نوع (type) را هم به آن اضافه می کند.
اگر دقت کنید می بینید که ما دوبار از push در آرایه استفاده کردیم.
اینکار به این خاطر است که apiی bulk انتظار دارد یک آبجکت شامل تعریف ایندکس در ابتدا، و سپس سندی که می خواهید ایندکس کنید را به عنوان پارامتر دوم دریافت کند.
برای کسب اطلاع بیشتر می توانید اینجا را بررسی کنید.
سپس متد client.bulk را تعریف کردیم و یک آرایه bulk جدید به متد پاس میدهیم. با اینکار تمام داده های تان با ایندکس roxo.ir-tutorials و از نوع cities_list در ElasticSearch ایندکس می شود.
نمونه instance ElasticSearch آماده بکار است. و می توانید با Node.js به آن متصل شوید. حال می خواهیم با استفاده از Express یک صفحه فرود (landing page) طراحی کنیم. برای اینکار یک فایل با نام index.js ایجاد کرده و کدهای زیر را به آن اضافه کنید.
//index.js //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' )); } );
تشریح کد بالا
بدنه جستجو به جز query می تواند شامل پروپرتی های اختیاری دیگری هم باشد، مثل size و from.
پروپرتی size تعداد اسنادی که در پاسخ جستجو ارسال می شود را مشخص می کند. اگر هیچ مقداری را بر نگرداند، بطور پیش فرض 10 صفحه برگشت داده می شود.
پروپرتی from ایندکس شروع اسناد برگشت داده شده را مشخص می کند. این پروپرتی ها برای صفحه بندی کاربرد دارند.
اگر بخواهید پاسخ ها را از 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] ] } }
در بالا، پروپرتی took تعداد میلی ثانیه هایی است که برای پیدا کردن جواب صرف شده است.
time_out وقتی true می شود که هیچ نتیجه ای یافت نشود، _shards برای اطلاع درباره وضعیت نودهای مختلف استفاده می شود .و در نهایت hits شامل نتایج جستجو است.
داخل پروپرتی hits یک آبجکت با پروپرتی های زیر را داریم:
total: تعداد کل آیتم های مطابقت داده شده است.
max_score: بیشترین امتیاز آیتم های یافت شده.
hits: یک آرایه شامل آیتم های یافت شده است.
ابتدا دو فولدر به نام های Views و public در روت پروژه ایجاد کنید. سپس یک فایل با نام template.html در فولدر 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 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>
در کد های بالا دو قسمت اصلی وجود دارد:
کدهای html : در این قسمت سه کتابخانه را وارد صفحه می کنیم.
1- فایل css بوت استرپ، برای استایل دهی صفحه
2- Axios.js : برای ساخت درخواست های http به سرور
3- Vue.js: یک فریم ورک سبک برای طراحی ظاهر برنامه
کدهای css: در اینجا کدی نوشتیم تا هنگامی که ماوس را به سمت فیلد جستجو ببرید، آن فیلد ظاهر شده و در صورت دور شدن از آن، پنهان می شود.
در مرحله بعد، یک input برای جستجو داریم که v-model آن را به query نسبت می دهیم. سپس با یک حلقه تکرار تمام نتایج جستجو را می گیریم.
حال دستور node index.js را اجرا کرده و به آدرس localhost:3001 مراجعه کنید. نتیجه مطابق تصویر زیر است:
سپس یک تگ script به فایل template.html اضافه می کنیم.
//template.html // 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(); } } })
در این قسمت یک نمونه جدید vue تعریف کردیم و آن را به یک عنصر با شناسه id app متصل کردیم. سپس پروپرتی های زیر را هم تعریف کردیم:
1- query: این پروپرتی را به فیلد جستجو متصل می کنیم.
2- results: یک آرایه شامل تمام نتایج جستجوهای یافت شده است.
سپس از یک پروپرتی تحت عنوان watcher در vue.js استفاده می کنیم. هر زمان که تغییری در کد بوجود بیاید، watcher ها یک عمل خاصی را انجام می دهند
.در اینجا تغییرات پروپرتی query را زیر نظر می گیریم و به محض انجام یک تغییر روی آن، متد search را اجرا می کنیم.
حال اگر دستور node index.js را مجدداً اجرا کرده و به آدرس localhost:3001 مراجعه کنید. باید نتیجه ای مانند زیر را مشاهده کنید.
ما می توانیم در سمت کلاینت و بطور مستقیم در 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 ایجاد کردیم. این روت فایل template2.html را برگشت می دهد.
سپس باید کتابخانه کلاینت ElasticSearch را از اینجا دانلود کنید. بعد از دانلود، فایل elasticsearch.min.js را به فولدر public پروژه تان کپی کنید.
نکته: در صورت اتصال به ElasticSearch از سمت کلاینت با خطای cors مواجه خواهید شد. برای حل این موضوع، کدهای زیر را در فایل پیکربندی ElasticSearch قرار دهید. برای کسب اطلاع بیشتر به این آدرس مراجعه کنید.
#/etc/elasticsearch/elasticsearch.yml http.cors.enabled : true http.cors.allow-origin : "*"
بعد از انجام کارهای فوق، نمونه ElasticSearch تان را ریستارت کنید.
// restart the Elasticsearch service sudo service elasticsearch restart
سپس یک فایل به نام template2.html در فولدر View ایجاد کنید.
<!-- 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>
سپس تگ script را به فایل template2.html اضافه کنید
//template2.html // 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(); } } })
کدهای html و جاوا اسکریپت بالا خیلی شبیه به بخش قبلی هستند با این تفاوت که :
1- به جای axios باید از elasticsearch.js استفاده کنید.
2- در بالای تگ Script ، کلاینت ElasticSearch را مقداردهی اولیه میکنیم.
متد search یک درخواست Http را اجرا نمی کند، بلکه عمل جستجو توسط خود elasticsearch و در سمت سرور انجام می شود.
حال اگر به آدرس localhost:3001/v2 مراجعه کنید، باید تصویر زیر را ببینید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.