https://www.sitepoint.com/building-recipe-search-site-angular-elasticsearch/


ByAdam BardApril 15,2014


Have you ever wanted to build a search feature into an application? In the old days,you might have found yourself wrangling withSolr,or building your own search service on top ofLucene— if you were lucky. But,since 2010,there’s been an easier way:Elasticsearch.

Elasticsearch is an open-source storage engine built on Lucene. It’s more than a search engine; it’s a true document store,albeit one emphasizing search performance over consistency or durability. This means that,for many applications,you can use Elasticsearch as your entire backend. Applications such as…

Building a Recipe Search Engine

In this article,you’ll learn how to use Elasticsearch withAngularJSto create a search engine for recipes,just like the one atOpenRecipeSearch.com. Why recipes?

  1. OpenRecipesexists,which makes our job a lot easier.
  2. Why not?

OpenRecipes is an open-source project that scrapes a bunch of recipe sites for recipes,then provides them for download in a handy JSON format. That’s great for us,because Elasticsearch uses JSON too. However,we have to get Elasticsearch up and running before we can Feed it all those recipes.

Download Elasticsearchand unzip it into whatever directory you like. Next,open a terminal,cdto the directory you just unzipped,and runbin/elasticsearch(bin/elasticsearch.baton Windows). Ta-da! You’ve just started your very own elasticsearch instance. Leave that running while you follow along.

One of the great features of Elasticsearch is its out-of-the-Box RESTful backend,which makes it easy to interact with from many environments. We’ll be using theJavaScript driver,but you Could usewhichever one you like; the code is going to look very similar either way. If you like,you can refer to thishandy reference(disclaimer: written by me).

Now,you’ll need a copy of theOpenRecipes database. It’s just a big file full of JSON documents,so it’s straightfoward to write a quick Node.js script to get them in there. You’ll need to get the JavaScript Elasticsearch library for this,so runnpm install elasticsearch. Then,create a file namedload_recipes.js,and add the following code.

var fs = require('fs');
var es 'elasticsearch'var client = new es.Client({
  host: 'localhost:9200'
};

fs.readFile'recipeitems-latest.json', {encoding'utf-8'function(err) {
  if { throw err; }

  // Build up a giant bulk request for elasticsearch.
  bulk_request = datasplit'\n'reduce((bulk_request{
    var obj;

    try {
      obj = JSONparse(line;
    } catch(e{
      consolelog'Done reading';
      return bulk_request}

    // Rework the data slightly
    recipe = {
      id: obj._id.$oid// Was originally a mongodb entry
      name.name.source.url.recipeYield.ingredients.prepTime.cookTime.datePublished.description
    ;

    bulk_requestpush{index: {_index'recipes''recipe': recipe.id;
    bulk_request(recipe;
    ;
  [];

  // A little voodoo to simulate synchronous insert
  var busy = false;
  var callback { console}

    busy // Recursively whittle away at bulk_request,1000 at a time.
  var perhaps_insert (!busy{
      busy true;
      clientbulk{
        body: bulk_requestslice(01000)
      ;
      bulk_request = bulk_request;
      console.length}

    .length > {
      setTimeout(perhaps_insert10else 'Inserted all records.'}
  ;

  perhaps_insert;
;

Next,run the script using the commandnode load_recipes.js. 160,000 records later,we have a full database of recipes ready to go. Check it out withcurlif you have it handy:

$ curl -XPOST http://localhost:9200/recipes/recipe/_search -d '{"query": {"match": {"_all": "cake"}}}'

ottom:32px; padding-top:0px; padding-bottom:0px; direction:ltr; font-family:Roboto,you might be OK usingcurlto search for recipes,but if the world is going to love your recipe search,you’ll need to…

Build a Recipe Search UI

This is where Angular comes in. I chose Angular for two reasons: because I wanted to,and because Elasticsearch’s JavaScript library comes with an experimental Angular adapter. I’ll leave the design as an exercise to the reader,but I’ll show you the important parts of the HTML.

Get your hands on Angular and Elasticsearch Now. I recommendBower,but you can just download them too. Open yourindex.htmlfile and insert them wherever you usually put your JavaScript (I prefer just before the closingbodytag myself,but that’s a whole other argument):

<script src="path/to/angular/angular.js"></script>
"path/to/elasticsearch/elasticsearch.angular.js>

ottom:32px; padding-top:0px; padding-bottom:0px; direction:ltr; font-family:Roboto,let’s stop to think about how our app is going to work:

  1. The user enters a query.
  2. We send the query as a search to Elasticsearch.
  3. We retrieve the results.
  4. We render the results for the user.

The following code sample shows the key HTML for our search engine,with Angular directives in place. If you’ve never used Angular,that’s OK. You only need to kNow a few things to grok this example:

  1. HTML attributes starting withngare Angular directives.
  2. The dynamic parts of your Angular applications are enclosed with anng-appand anng-controller.ng-appandng-controllerdon’t need to be on the same element,but they can be.
  3. All other references to variables in the HTML refer to properties on the$scopeobject that we’ll meet in the JavaScript.
  4. The parts enclosed in{{}}are template variables,like in Django/Jinja2/Liquid/Mustache templates.
<div ng-app"myOpenRecipes" ng-controller"recipeCtrl>

  <!-- The search Box puts the term into $scope.searchTerm and calls $scope.search() on submit -->
  <section class"searchField>
    <form ng-submit"search()>
      <input type"textng-model"searchTerm"submitvalue"Search for recipes</form>
  </section<!-- In results,we show a message if there are no results,and a list of results otherwise. -->
  "results"no-recipesng-hide"recipes.length>No results</div>

    <!-- We show one of these elements for each recipe in $scope.recipes. The ng-cloak directive prevents our templates from showing on load. -->
    <article "recipeng-repeat"recipe in recipesng-cloak<h2>
        <a ng-href"{{recipe.url}}>{{recipe.name}}</a</h2<ul<li "ingredient in recipe.ingredients>{{ ingredient }}</li</ul>

      <p>
        {{recipe.description}}
        >... more at {{recipe.source}}</p</article<!-- We put a link that calls $scope.loadMore to load more recipes and append them to the results.-->
    "load-more"allResultsng-click"loadMore()>More...myOpenRecipes(via theng-appattribute). 
/** * Create the module. Set it up to use html5 mode. */
window.MyOpenRecipes = angularmodule'myOpenRecipes'['$locationProvider'($locationProvider{
    $locationProviderhtml5Mode(]
 For those new to Angular,the['$locationProvider',function($locationProvider) {...}]business is our way of telling Angular that we’d like it to pass$locationProviderto our handler function so we can use it. This system of dependency injection removes the need for us to rely on global variables (except the globalangularand theMyOpenRecipeswe just created). 

ottom:32px; padding-top:0px; padding-bottom:0px; direction:ltr; font-family:Roboto,we’ll write the controller,namedrecipeCtrl. We need to make sure to initialize therecipes,240)">allResults,andsearchTermvariables used in the template,as well as providingsearch()andloadMore()as actions.

/** * Create a controller to interact with the UI. */
MyOpenRecipescontroller'recipeCtrl''recipeService''$scope''$location'(recipescope{ // Provide some nice initial choices var initChoices [ "rendang""nasi goreng""pad thai""pizza""lasagne""ice cream""schnitzel""hummous" var idx = Mathfloor(Mathrandom) * initChoices// Initialize the scope defaults. $scope.recipes ; // An array of recipe results to display $scope.page = ; // A counter to keep track of our current page $scope.allResults ; // Whether or not all results have been found. // And,a random search term to start if none was present on page load. $scope.searchTerm = $locationsearch.q || initChoices[idx/** * A fresh search. Reset the scope variables to their defaults,set * the q query parameter,and load more results. */ $scope.search { $scope; $scope; $location{'q': $scope.searchTermloadMore/** * Load the next page of results,incrementing the page counter. * When query is finished,push results onto $scope.recipes and decide * whether all results have been returned (i.e. were 10 results returned?) */ $scope.loadMore { recipes($scope.page++then(results{ !== { $scope; } var ii ; for ; ii < results; ii.recipes[ii} // Load results on first run $scope You should recognize everything on the$scopeobject from the HTML. Notice that our actual search query relies on a mysterIoUs object calledrecipeService. A service is Angular’s way of providing reusable utilities for doing things like talking to outside resources. Unfortunately,Angular doesn’t providerecipeService,so we’ll have to write it ourselves. Here’s what it looks like:
MyOpenRecipesfactory'$q''esFactory'($qelasticsearch{
    host: $locationhost+ ':9200'
  /** * Given a term and an offset,load another round of 10 recipes. * * Returns a promise. */
  var search (termvar deferred = $qdefervar query {
      match{
        _all: term
      ;

    client{
      index{
        size: from(offset || * : query
      (result;

      hits_in .hits || {for< hits_in{
        hits_out(hits_in._source}

      deferredresolve(hits_out.rejectreturn deferred.promise// Since this is a factory method,we return an object representing the actual service.
  return {
    search: search
   Our service is quite barebones. It exposes a single method,240)">search(),that allows us to send a query to Elasticsearch’s,searching across all fields for the given term. You can see that in thequerypassed in the body of the call tosearch:{"match": {"_all": term}}._allis a special keyword that lets us search all fields. If instead,our query was{"match": {"title": term}},we would only see recipes that contained the search term in the title. 

The results come back in order of decreasing “score”,which is Elasticsearch’s guess at the document’s relevance based on keyword frequency and placement. For a more complicated search,we Could tune the relative weights of the score (i.e. a hit in the title is worth more than in the description),but the default seems to do pretty well without it.

You’ll also notice that the search accepts anoffsetargument. Since the results are ordered,we can use this to fetch more results if requested by telling Elasticsearch to skip the firstnresults.

Some Notes on Deployment

Deployment is a bit beyond the scope of this article,but if you want to take your recipe search live,you need to be careful. Elasticsearch has no concept of users or permissions. If you want to prevent just anyone from adding or deleting recipes,you’ll need to find some way to prevent access to those REST endpoints on your Elasticsearch instance. For example,OpenRecipeSearch.comuses Nginx as a proxy in front of Elasticsearch to prevent outside access to all endpoints butrecipes/recipe/_search.

Congratulations,You’ve Made a Recipe Search

ottom:32px; padding-top:0px; padding-bottom:0px; direction:ltr; font-family:Roboto,if you openindex.htmlin a browser,you should see an unstyled list of recipes,since our controller fetches some randomly for you on page load. If you enter a new search,you’ll get 10 results relating to whatever you searched for,and if you click “More…” at the bottom of the page,some more recipes should appear (if there are indeed more recipes to fetch).

That’s all there is to it! You can find all the necessary files to run this project onGitHub.

Building a Recipe Search Site with Angular and Elasticsearch的更多相关文章

  1. Android Studio是否支持用于Android UI设计的AngularJS?

    我对AndroidStudio有疑问:AS在设计XML文件时是否支持AngularJS代码,例如:对于小动画或效果?

  2. android – 如何使用ClientID和ClientSecret在Phonegap中使用Angularjs登录Google OAuth2

    我正尝试使用Angularjs(使用IonicFramework)通过GoogleOAuth2从我的Phonegap应用程序登录.目前我正在使用http://phonegap-tips.com/articles/google-api-oauth-with-phonegaps-inappbrowser.html进行登录.但是当我使用Angular-UI-RouterforIonic时,它正在创建非常

  3. 利用require.js与angular搭建spa应用的方法实例

    这篇文章主要给大家介绍了关于利用require.js与angular搭建spa应用的方法实例,文中通过示例代码给大家介绍的非常详细,对大家的理解和学习具有一定的参考学习价值,需要的朋友们下面跟着小编来一起看看吧。

  4. 详解Angular动态组件

    本文主要介绍了Angular动态组件,对此感兴趣的同学,可以亲自实验一下。

  5. 详解如何使用webpack+es6开发angular1.x

    本篇文章主要介绍了详解如何使用webpack+es6开发angular1.x,具有一定的参考价值,有兴趣的可以了解一下

  6. angular2系列之路由转场动画的示例代码

    本篇文章主要介绍了angular2系列之路由转场动画的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. 一种angular的方法级的缓存注解(装饰器)

    本篇文章主要介绍了一种angular的方法级的缓存注解(装饰器),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  8. 动手写一个angular版本的Message组件的方法

    本篇文章主要介绍了动手写一个angular版本的Message组件的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  9. angular forEach方法遍历源码解读

    这篇文章主要为大家详细了angular forEach方法遍历源码,forEach()方法用于遍历对象或数组,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Angular的MVC和作用域

    本文主要Angular的MVC和作用域进行详细分析介绍,具有一定的参考价值,下面跟着小编一起来看下吧

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部