├── .gitignore
├── ElasticsearchRecipes.sln
├── README.md
├── app_full.png
├── azure-pipelines.yml
├── example_query_result.png
├── global.json
└── src
└── ElasticsearchRecipes
├── Controllers
├── Api
│ ├── ApiController.cs
│ ├── IndexController.cs
│ └── RecipeController.cs
└── Application
│ └── HomeController.cs
├── Elastic
├── DataIndexer.cs
├── ElasticClientProvider.cs
├── ElasticConnectionSettings.cs
└── SearchService.cs
├── ElasticsearchRecipes.xproj
├── ElasticsearchRecipes.xproj.user
├── Models
├── AutocompleteResult.cs
├── IndexResult.cs
├── Recipe.cs
└── SearchResult.cs
├── Program.cs
├── Project_Readme.html
├── Properties
└── launchSettings.json
├── Startup.cs
├── appsettings.json
├── project.json
├── project.lock.json
├── web.config
└── wwwroot
├── App.js
├── Controllers
├── DetailsController.js
├── MainController.js
├── MoreLikeThisController.js
└── SearchController.js
├── Routes.js
├── Services
└── RecipeService.js
├── Views
├── Details.html
├── Main.html
├── MoreLikeThis.html
└── SearchResult.html
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnikolovv/elasticsearch-recipes-nest-angular/fa561ce49f4140609dcfdbfc904a2b3a5b616a20/.gitignore
--------------------------------------------------------------------------------
/ElasticsearchRecipes.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.25420.1
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7F8C2D24-44C5-453F-A6DE-C01B79C91C06}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8D8DD44B-1E77-458A-9A58-622E591267D5}"
9 | ProjectSection(SolutionItems) = preProject
10 | global.json = global.json
11 | EndProjectSection
12 | EndProject
13 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ElasticsearchRecipes", "src\ElasticsearchRecipes\ElasticsearchRecipes.xproj", "{58BDEA3A-766D-4501-8DA5-AA3F78939980}"
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Release|Any CPU = Release|Any CPU
19 | EndGlobalSection
20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
21 | {58BDEA3A-766D-4501-8DA5-AA3F78939980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
22 | {58BDEA3A-766D-4501-8DA5-AA3F78939980}.Debug|Any CPU.Build.0 = Debug|Any CPU
23 | {58BDEA3A-766D-4501-8DA5-AA3F78939980}.Release|Any CPU.ActiveCfg = Release|Any CPU
24 | {58BDEA3A-766D-4501-8DA5-AA3F78939980}.Release|Any CPU.Build.0 = Release|Any CPU
25 | EndGlobalSection
26 | GlobalSection(SolutionProperties) = preSolution
27 | HideSolutionNode = FALSE
28 | EndGlobalSection
29 | GlobalSection(NestedProjects) = preSolution
30 | {58BDEA3A-766D-4501-8DA5-AA3F78939980} = {7F8C2D24-44C5-453F-A6DE-C01B79C91C06}
31 | EndGlobalSection
32 | EndGlobal
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > This is the finished application if you were to follow my [4 part tutorial](https://devadventures.net/2018/04/16/connect-your-asp-net-core-application-to-elasticsearch-using-nest-5/) on Elasticsearch and NEST
2 |
3 | # The application
4 | The application is pretty simple, but extremely flexible in terms of searching data.
5 |
6 | ![Application][app_full]
7 |
8 | Behind the scenes, the user inputted query is converted to a [query_string][qsdocs] query that is sent to Elastic. The [query_string][qsdocs] query provides native support for a number of [search syntax][qssearchsyntax] options, but we're mostly interested in [wildcards][qswildcards], [boolean operators][qsbooloperators], [phrase matching][qsphrasematching] and [boosting][qsboosting].
9 |
10 | The user inputted string is proccessed in the following way:
11 | > * Exact phrases to look for are marked with ***quotes*** ("***an example phrase***").
12 | > * Each word/phrase that isn't marked in any way (**example**) ***will*** be present in the result set's ingredients.
13 | > * Each word/phrase that is marked with a minus (**-example**) ***will not*** be present in the result set's ingredients.
14 | > * Each word/phrase can be [boosted][qsboosting] to become more relevant (score higher) in the search results.
15 |
16 | So, if we want to look for recipes containing ***garlic***, ***tomatoes***, ***chopped onions*** and *not* containing ***eggs*** and ***red-pepper flakes***, we'll have to input the query string:
17 |
18 | > **garlic^2 tomatoes -egg* "chopped onions" -"red-pepper flakes"** // Order doesn't matter
19 |
20 | As you can see, we've boosted the garlic. This means that recipes where garlic appears more frequently will score higher.
21 |
22 | The asterisk symbol following the ***egg*** is called a **wildcard** query. It means that it's going to look for words that start with ***egg*** and continue with anything else, *no matter the length* (for example, **egg*** is going to match **eggs, eggplant, eggbeater**, etc.), very similar to SQL's like.
23 |
24 | You can substitute the asterisk with a "**?**" sign, which will indicate that Elastic should look for just a **single** missing letter.
25 |
26 | This spits out the result:
27 | ![Result][sample_query_result]
28 | ...
29 |
30 | # How to run
31 |
32 | In order to run the application, you must [install and run][install_elastic_url] elastic then execute the following steps:
33 |
34 | 1. Clone the repository.
35 | 2. Open the project in Visual Studio and restore packages (Ctrl + Shift + K, Ctrl + Shift + R).
36 | 3. Run the application (Ctrl + F5).
37 |
38 | In order to make use of the search functionality, you must have some recipes indexed in your elastic cluster.
39 |
40 | First, make sure that the cluster url in `appsettings.json` is correctly configured then:
41 |
42 | 1. Download the [openrecipes dump][openrecipes_dump_download] json file.
43 | 2. Extract the json file in `project-root\src\ElasticsearchRecipes\Data` (create the Data folder if it doesn't exist).
44 | 3. While the application is running, make a get request to `appurl/api/index/file?fileName=openrecipes-big.json`.
45 | 4. In a few minutes you should see a json response confirming that the indexation was valid.
46 |
47 | Everything should be set-up and you are free to play around.
48 |
49 | [app_full]: ./app_full.png
50 | [sample_query_result]: ./example_query_result.png
51 | [install_elastic_url]: https://www.elastic.co/guide/en/elasticsearch/guide/current/running-elasticsearch.html
52 | [openrecipes_dump_download]: https://drive.google.com/open?id=0B-HzAYDi4IbYR0dOek1MWFVkRFU
53 |
54 | [qsdocs]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
55 | [qssearchsyntax]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
56 | [qsbooloperators]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_boolean_operators
57 | [qsboosting]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_boosting
58 | [qswildcards]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_wildcards
59 | [qsphrasematching]: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
60 |
--------------------------------------------------------------------------------
/app_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnikolovv/elasticsearch-recipes-nest-angular/fa561ce49f4140609dcfdbfc904a2b3a5b616a20/app_full.png
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # Starter pipeline
2 | # Start with a minimal pipeline that you can customize to build and deploy your code.
3 | # Add steps that build, run tests, deploy, and more:
4 | # https://aka.ms/yaml
5 |
6 | trigger:
7 | - master
8 |
9 | pool:
10 | vmImage: 'Ubuntu-16.04'
11 |
12 | steps:
13 | - script: echo Hello, world!
14 | displayName: 'Run a one-line script'
15 |
16 | - script: |
17 | echo Add other tasks to build, test, and deploy your project.
18 | echo See https://aka.ms/yaml
19 | displayName: 'Run a multi-line script'
20 |
--------------------------------------------------------------------------------
/example_query_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dnikolovv/elasticsearch-recipes-nest-angular/fa561ce49f4140609dcfdbfc904a2b3a5b616a20/example_query_result.png
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "projects": [ "src", "test" ],
3 | "sdk": {
4 | "version": "1.0.0-preview2-003131"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Controllers/Api/ApiController.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Controllers.Api
2 | {
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | ///
6 | /// Just used as a shortcut to achieve a global route prefix.
7 | ///
8 | [Route("/api/[controller]")]
9 | public class ApiController : Controller
10 | {
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Controllers/Api/IndexController.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Controllers.Api
2 | {
3 | using Elastic;
4 | using Microsoft.AspNetCore.Mvc;
5 | using System.Threading.Tasks;
6 |
7 | public class IndexController : ApiController
8 | {
9 | public IndexController(DataIndexer indexer)
10 | {
11 | this.indexer = indexer;
12 | }
13 |
14 | private readonly DataIndexer indexer;
15 |
16 | ///
17 | /// The file must be present in the project's Data directory
18 | ///
19 | ///
20 | ///
21 | ///
22 | [HttpGet("file")]
23 | public async Task IndexDataFromFile([FromQuery]string fileName, string index, bool deleteIndexIfExists)
24 | {
25 | var response = await this.indexer.IndexRecipesFromFile(fileName, deleteIndexIfExists, index);
26 | return Json(response);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Controllers/Api/RecipeController.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Controllers.Api
2 | {
3 | using Elastic;
4 | using Microsoft.AspNetCore.Mvc;
5 | using System.Threading.Tasks;
6 |
7 | public class RecipeController : ApiController
8 | {
9 | public RecipeController(SearchService searchService)
10 | {
11 | this.searchService = searchService;
12 | }
13 |
14 | private readonly SearchService searchService;
15 |
16 | [HttpGet("search")]
17 | public async Task Search([FromQuery]string query, int page = 1, int pageSize = 10)
18 | {
19 | var result = await this.searchService.Search(query, page, pageSize);
20 | return Json(result);
21 | }
22 |
23 | [HttpGet("{id?}")]
24 | public async Task GetById([FromQuery]string id)
25 | {
26 | var result = await this.searchService.GetById(id);
27 | return Json(result);
28 | }
29 |
30 | [HttpGet("morelikethis")]
31 | public async Task MoreLikeThis([FromQuery]string id, int page = 1, int pageSize = 10)
32 | {
33 | var result = await this.searchService.MoreLikeThis(id, page, pageSize);
34 | return Json(result);
35 | }
36 |
37 | [HttpGet("autocomplete")]
38 | public async Task Autocomplete([FromQuery]string query)
39 | {
40 | var result = await this.searchService.Autocomplete(query);
41 | return Json(result);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Controllers/Application/HomeController.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Controllers.Application
2 | {
3 | using Microsoft.AspNetCore.Mvc;
4 |
5 | public class HomeController : Controller
6 | {
7 | // A catch-all route is registered in Startup.cs so that every url leads to this action
8 | public IActionResult Recipes()
9 | {
10 | // The angular app entry point
11 | return File("~/index.html", "text/html");
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Elastic/DataIndexer.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Elastic
2 | {
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.Options;
5 | using Models;
6 | using Nest;
7 | using Newtonsoft.Json;
8 | using System;
9 | using System.Diagnostics;
10 | using System.IO;
11 | using System.Linq;
12 | using System.Threading.Tasks;
13 |
14 | public class DataIndexer
15 | {
16 | public DataIndexer(ElasticClientProvider clientProvider, IHostingEnvironment env, IOptions settings)
17 | {
18 | this.client = clientProvider.Client;
19 | this.contentRootPath = Path.Combine(env.ContentRootPath, "data");
20 | this.defaultIndex = settings.Value.DefaultIndex;
21 | }
22 |
23 | private readonly ElasticClient client;
24 | private readonly string contentRootPath;
25 | private readonly string defaultIndex;
26 |
27 | public async Task IndexRecipesFromFile(string fileName, bool deleteIndexIfExists, string index = null)
28 | {
29 | SanitizeIndexName(ref index);
30 | Recipe[] mappedCollection = await ParseJsonFile(fileName);
31 | await DeleteIndexIfExists(index, deleteIndexIfExists);
32 | await CreateIndexIfItDoesntExist(index);
33 | await ConfigurePagination(index);
34 | return await IndexDocuments(mappedCollection, index);
35 | }
36 |
37 | private void SanitizeIndexName(ref string index)
38 | {
39 | // The index must be lowercase, this is a requirement from Elastic
40 | if (index == null)
41 | {
42 | index = this.defaultIndex;
43 | }
44 | else
45 | {
46 | index = index.ToLower();
47 | }
48 | }
49 |
50 | private async Task IndexDocuments(Recipe[] mappedCollection, string index)
51 | {
52 | int batchSize = 10000; // magic
53 | int totalBatches = (int)Math.Ceiling((double)mappedCollection.Length / batchSize);
54 |
55 | for (int i = 0; i < totalBatches; i++)
56 | {
57 | var response = await this.client.IndexManyAsync(mappedCollection.Skip(i * batchSize).Take(batchSize), index);
58 |
59 | if (!response.IsValid)
60 | {
61 | return new IndexResult
62 | {
63 | IsValid = false,
64 | ErrorReason = response.ServerError?.Error?.Reason,
65 | Exception = response.OriginalException
66 | };
67 | }
68 | else
69 | {
70 | Debug.WriteLine($"Successfully indexed batch {i + 1}");
71 | }
72 | }
73 |
74 | return new IndexResult
75 | {
76 | IsValid = true
77 | };
78 | }
79 |
80 | private async Task ConfigurePagination(string index)
81 | {
82 | // Max out the result window so you can have pagination for >100 pages
83 | await this.client.UpdateIndexSettingsAsync(index, ixs => ixs
84 | .IndexSettings(s => s
85 | .Setting("max_result_window", int.MaxValue)));
86 | }
87 |
88 | private async Task CreateIndexIfItDoesntExist(string index)
89 | {
90 | if (!this.client.IndexExists(index).Exists)
91 | {
92 | var indexDescriptor = new CreateIndexDescriptor(index)
93 | .Mappings(mappings => mappings
94 | .Map(m => m.AutoMap()));
95 |
96 | await this.client.CreateIndexAsync(index, i => indexDescriptor);
97 | }
98 | }
99 |
100 | private async Task DeleteIndexIfExists(string index, bool shouldDeleteIndex)
101 | {
102 | if (this.client.IndexExists(index).Exists && shouldDeleteIndex)
103 | {
104 | await this.client.DeleteIndexAsync(index);
105 | }
106 | }
107 |
108 | private async Task ParseJsonFile(string fileName)
109 | {
110 | using (FileStream fs = new FileStream(Path.Combine(contentRootPath, fileName), FileMode.Open))
111 | {
112 | using (StreamReader reader = new StreamReader(fs))
113 | {
114 | // Won't be efficient with large files
115 | string rawJsonCollection = await reader.ReadToEndAsync();
116 |
117 | Recipe[] mappedCollection = JsonConvert.DeserializeObject(rawJsonCollection, new JsonSerializerSettings
118 | {
119 | Error = HandleDeserializationError
120 | });
121 |
122 | return mappedCollection;
123 | }
124 | }
125 | }
126 |
127 | // https://stackoverflow.com/questions/26107656/ignore-parsing-errors-during-json-net-data-parsing
128 | private void HandleDeserializationError(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs errorArgs)
129 | {
130 | var currentError = errorArgs.ErrorContext.Error.Message;
131 | errorArgs.ErrorContext.Handled = true;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Elastic/ElasticClientProvider.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Elastic
2 | {
3 | using Microsoft.Extensions.Options;
4 | using Nest;
5 |
6 | public class ElasticClientProvider
7 | {
8 | public ElasticClientProvider(IOptions settings)
9 | {
10 | ConnectionSettings connectionSettings =
11 | new ConnectionSettings(new System.Uri(settings.Value.ClusterUrl));
12 |
13 | connectionSettings.EnableDebugMode();
14 |
15 | if (settings.Value.DefaultIndex != null)
16 | {
17 | connectionSettings.DefaultIndex(settings.Value.DefaultIndex);
18 | }
19 |
20 | this.Client = new ElasticClient(connectionSettings);
21 | }
22 |
23 | public ElasticClient Client { get; }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Elastic/ElasticConnectionSettings.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Elastic
2 | {
3 | public class ElasticConnectionSettings
4 | {
5 | public string ClusterUrl { get; set; }
6 |
7 | public string DefaultIndex
8 | {
9 | get { return defaultIndex; }
10 | set { defaultIndex = value.ToLower(); }
11 | }
12 |
13 | private string defaultIndex;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Elastic/SearchService.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Elastic
2 | {
3 | using Models;
4 | using Nest;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Threading.Tasks;
8 |
9 | public class SearchService
10 | {
11 | public SearchService(ElasticClientProvider clientProvider)
12 | {
13 | this.client = clientProvider.Client;
14 | }
15 |
16 | private readonly ElasticClient client;
17 |
18 | ///
19 | /// Searches elastic for recipes matching the given query. If a word in the query is preceeded by a '-' sign, the results won't contain it. Supports everything QueryStringQuery does,
20 | /// (wildcard queries, phrase matching, proximity matching, etc.)
21 | ///
22 | ///
23 | ///
24 | ///
25 | ///
26 | public async Task> Search(string query, int page, int pageSize)
27 | {
28 | #region RawQuery
29 | /*
30 | Passed query: "chopped onions" eggs -tomatoes -"olive oil"
31 | {
32 | "from": 0,
33 | "size": 10,
34 | "query": {
35 | "bool": {
36 | "must": [
37 | {
38 | "query_string": {
39 | "query": "\"chopped onions\" eggs -tomatoes -\"olive oil\""
40 | }
41 | }
42 | ]
43 | }
44 | }
45 | }
46 | */
47 | #endregion
48 |
49 | var response = await this.client.SearchAsync(searchDescriptor => searchDescriptor
50 | .Query(queryContainerDescriptor => queryContainerDescriptor
51 | .Bool(queryDescriptor => queryDescriptor
52 | .Must(queryStringQuery => queryStringQuery
53 | .QueryString(queryString => queryString
54 | .Query(query)))))
55 | .From((page - 1) * pageSize)
56 | .Size(pageSize));
57 |
58 | return MapResponseToSearchResult(response, page, pageSize);
59 | }
60 |
61 | public async Task> MoreLikeThis(string id, int page, int pageSize)
62 | {
63 | #region RawQuery
64 | /*
65 | Raw query:
66 | {
67 | "from": (page - 1) * pageSize,
68 | "size": pageSize,
69 | "query": {
70 | "more_like_this": {
71 | "fields": [
72 | "ingredients"
73 | ],
74 | "like": [
75 | {
76 | "_index": "recipes",
77 | "_type": "recipe",
78 | "_id": "id"
79 | }
80 | ]
81 | }
82 | }
83 | }
84 | */
85 | #endregion
86 |
87 | var response = await this.client.SearchAsync(s => s
88 | .Query(q => q
89 | .MoreLikeThis(qd => qd
90 | .Like(l => l.Document(d => d.Id(id)))
91 | .Fields(fd => fd.Fields(r => r.Ingredients))))
92 | .From((page - 1) * pageSize)
93 | .Size(pageSize));
94 |
95 | return MapResponseToSearchResult(response, page, pageSize);
96 | }
97 |
98 | public async Task GetById(string id)
99 | {
100 | var response = await this.client.GetAsync(id);
101 | return response.Source;
102 | }
103 |
104 | public async Task> Autocomplete(string query)
105 | {
106 | #region RawQuery
107 | /*
108 | Raw query:
109 | {
110 | "suggest": {
111 | "recipe-name-completion": {
112 | "prefix": "query",
113 | "completion": {
114 | "field": "name",
115 | "fuzzy": {
116 | "fuzziness": "AUTO"
117 | }
118 | }
119 | }
120 | }
121 | }
122 | */
123 | #endregion
124 |
125 | var suggestionResponse = await this.client.SearchAsync(sr => sr
126 | .Suggest(scd => scd
127 | .Completion("recipe-name-completion", cs => cs
128 | .Prefix(query)
129 | .Fuzzy(fsd => fsd
130 | .Fuzziness(Fuzziness.Auto))
131 | .Field(r => r.Name))));
132 |
133 | List suggestions = this.ExtractAutocompleteSuggestions(suggestionResponse);
134 |
135 | return suggestions;
136 | }
137 |
138 | private List ExtractAutocompleteSuggestions(ISearchResponse response)
139 | {
140 | List results = new List();
141 |
142 | var matchingOptions = response.Suggest["recipe-name-completion"].Select(s => s.Options);
143 |
144 | foreach (var option in matchingOptions)
145 | {
146 | results.AddRange(option.Select(opt => new AutocompleteResult() { Id = opt.Source.Id, Name = opt.Source.Name }));
147 | }
148 |
149 | return results;
150 | }
151 |
152 | private SearchResult MapResponseToSearchResult(ISearchResponse response, int page, int pageSize)
153 | {
154 | return new SearchResult
155 | {
156 | IsValid = response.IsValid,
157 | ErrorMessage = response.ApiCall.OriginalException?.Message,
158 | Total = response.Total,
159 | ElapsedMilliseconds = response.Took,
160 | Page = page,
161 | PageSize = pageSize,
162 | Results = response?.Documents
163 | };
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/ElasticsearchRecipes.xproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 14.0
5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
6 |
7 |
8 |
9 | 58bdea3a-766d-4501-8da5-aa3f78939980
10 | ElasticsearchRecipes
11 | .\obj
12 | .\bin\
13 | v4.5.2
14 |
15 |
16 | 2.0
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/ElasticsearchRecipes.xproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ElasticsearchRecipes
5 | false
6 |
7 |
8 | ProjectDebugger
9 |
10 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Models/AutocompleteResult.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Models
2 | {
3 | public class AutocompleteResult
4 | {
5 | public string Id { get; set; }
6 |
7 | public string Name { get; set; }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Models/IndexResult.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Models
2 | {
3 | using System;
4 |
5 | public class IndexResult
6 | {
7 | public bool IsValid { get; set; }
8 |
9 | public string ErrorReason { get; set; }
10 |
11 | public Exception Exception { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Models/Recipe.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Models
2 | {
3 | using Nest;
4 | using System;
5 |
6 | [ElasticsearchType(Name = "recipe")]
7 | public class Recipe
8 | {
9 | public string Id { get; set; }
10 |
11 | [Completion]
12 | public string Name { get; set; }
13 | [Text]
14 | public string Ingredients { get; set; }
15 |
16 | public string Url { get; set; }
17 |
18 | public string Image { get; set; }
19 |
20 | public string CookTime { get; set; }
21 |
22 | public string RecipeYield { get; set; }
23 |
24 | public DateTime? DatePublished { get; set; }
25 |
26 | public string PrepTime { get; set; }
27 | [Text]
28 | public string Description { get; set; }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Models/SearchResult.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes.Models
2 | {
3 | using System.Collections.Generic;
4 |
5 | public class SearchResult
6 | {
7 | public bool IsValid { get; set; }
8 |
9 | public string ErrorMessage { get; set; }
10 |
11 | public long Total { get; set; }
12 |
13 | public int Page { get; set; }
14 |
15 | public int PageSize { get; set; }
16 |
17 | public IEnumerable Results { get; set; }
18 |
19 | public long ElapsedMilliseconds { get; set; }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Builder;
8 |
9 | namespace ElasticsearchRecipes
10 | {
11 | public class Program
12 | {
13 | public static void Main(string[] args)
14 | {
15 | var host = new WebHostBuilder()
16 | .UseKestrel()
17 | .UseContentRoot(Directory.GetCurrentDirectory())
18 | .UseIISIntegration()
19 | .UseStartup()
20 | .Build();
21 |
22 | host.Run();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Project_Readme.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Welcome to ASP.NET Core
6 |
127 |
128 |
129 |
130 |
138 |
139 |
140 |
141 |
This application consists of:
142 |
143 | Sample pages using ASP.NET Core MVC
144 | Bower for managing client-side libraries
145 | Theming using Bootstrap
146 |
147 |
148 |
160 |
161 |
Overview
162 |
171 |
172 |
173 |
Run & Deploy
174 |
179 |
180 |
181 |
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:56134/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "launchUrl": "api/values",
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "ElasticsearchRecipes": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "launchUrl": "http://localhost:5000/",
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/Startup.cs:
--------------------------------------------------------------------------------
1 | namespace ElasticsearchRecipes
2 | {
3 | using Elastic;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.StaticFilesEx;
7 | using Microsoft.Extensions.Configuration;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using Microsoft.Extensions.Logging;
10 |
11 | public class Startup
12 | {
13 | public Startup(IHostingEnvironment env)
14 | {
15 | var builder = new ConfigurationBuilder()
16 | .SetBasePath(env.ContentRootPath)
17 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
18 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
19 | .AddEnvironmentVariables();
20 | Configuration = builder.Build();
21 | }
22 |
23 | public IConfigurationRoot Configuration { get; }
24 |
25 | // This method gets called by the runtime. Use this method to add services to the container.
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | // Add framework services.
29 | services.AddMvc();
30 |
31 | // Get the connection settings from appsettings.json and inject them into ElasticConnectionSettings
32 | services.AddOptions();
33 | services.Configure(Configuration.GetSection("ElasticConnectionSettings"));
34 |
35 | services.AddSingleton(typeof(ElasticClientProvider));
36 | services.AddTransient(typeof(DataIndexer));
37 |
38 | services.AddTransient(typeof(SearchService));
39 | }
40 |
41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
42 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
43 | {
44 | loggerFactory.AddConsole(Configuration.GetSection("Logging"));
45 | loggerFactory.AddDebug();
46 |
47 | app.UseStaticFiles();
48 | app.UseMvc(routes =>
49 | {
50 | // Always serve the static index.html file, which is the entry point for the Angular app
51 | routes.MapRoute(
52 | name: "default",
53 | template: "{*url}",
54 | defaults: new
55 | {
56 | Controller = "Home",
57 | Action = "Recipes"
58 | });
59 | });
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "IncludeScopes": false,
4 | "LogLevel": {
5 | "Default": "Debug",
6 | "System": "Information",
7 | "Microsoft": "Information"
8 | }
9 | },
10 | "ElasticConnectionSettings": {
11 | "ClusterUrl": "http://localhost:9200",
12 | "DefaultIndex": "recipes"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "Microsoft.NETCore.App": {
4 | "version": "1.0.1",
5 | "type": "platform"
6 | },
7 | "Microsoft.AspNetCore.Mvc": "1.0.1",
8 | "Microsoft.AspNetCore.Routing": "1.0.1",
9 | "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
10 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
11 | "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
12 | "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
13 | "Microsoft.Extensions.Configuration.Json": "1.0.0",
14 | "Microsoft.Extensions.Logging": "1.0.0",
15 | "Microsoft.Extensions.Logging.Console": "1.0.0",
16 | "Microsoft.Extensions.Logging.Debug": "1.0.0",
17 | "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
18 | "Microsoft.AspNetCore.Diagnostics": "1.1.2",
19 | "Microsoft.AspNetCore.StaticFilesEx": "1.0.0.4",
20 | "NEST": "5.4.0"
21 | },
22 |
23 | "tools": {
24 | "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
25 | },
26 |
27 | "frameworks": {
28 | "netcoreapp1.0": {
29 | "imports": [
30 | "dotnet5.6",
31 | "portable-net45+win8"
32 | ]
33 | }
34 | },
35 |
36 | "buildOptions": {
37 | "emitEntryPoint": true,
38 | "preserveCompilationContext": true
39 | },
40 |
41 | "runtimeOptions": {
42 | "configProperties": {
43 | "System.GC.Server": true
44 | }
45 | },
46 |
47 | "publishOptions": {
48 | "include": [
49 | "wwwroot",
50 | "**/*.cshtml",
51 | "appsettings.json",
52 | "web.config"
53 | ]
54 | },
55 |
56 | "scripts": {
57 | "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/App.js:
--------------------------------------------------------------------------------
1 | var app = angular.module('app', ['ui.router', 'ui.bootstrap']);
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Controllers/DetailsController.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | app.controller('DetailsController', ['recipe', function (recipe) {
3 | var vm = this;
4 | vm.recipe = recipe;
5 | }]);
6 | })();
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Controllers/MainController.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | app.controller('MainController', ['$stateParams', '$state', 'RecipeService', function ($stateParams, $state, RecipeService) {
3 | var vm = this;
4 | vm.query = $stateParams.query;
5 |
6 | vm.pageSize = $stateParams.pageSize;
7 |
8 | vm.pageSizes = [
9 | 10,
10 | 20,
11 | 50,
12 | 100
13 | ];
14 |
15 | vm.completionSuggestions = function (query) {
16 | if (query.length > 0) {
17 |
18 | // In order to prevent uib-typeahead selecting the first item it encounters
19 | // we are always going to return the query typed by the user first, and then,
20 | // when the API responds with actual suggestions, we're going to append them
21 | var suggestions = [{ name: query }];
22 |
23 | return RecipeService.autocomplete(query).then(function (response) {
24 | // Append the suggestions to the user query
25 | return suggestions.concat(response.data);
26 | });
27 |
28 | return suggestions;
29 | }
30 | }
31 |
32 | vm.search = function () {
33 | var params = {
34 | query: vm.query,
35 | pageSize: vm.pageSize,
36 | page: 1
37 | };
38 |
39 | $state.go('recipes.search', params);
40 | }
41 |
42 | vm.goToSelectedItem = function (item) {
43 | if (item.id) {
44 | $state.go('recipes.details', { id: item.id });
45 | } else {
46 | // If an id is not specified, then the user has clicked his own query
47 | // (the first suggestion, which is automatically selected)
48 | // That means we should just excecute a search with the query that the user has entered
49 | vm.search();
50 | }
51 | }
52 | }]);
53 | })();
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Controllers/MoreLikeThisController.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | app.controller('MoreLikeThisController', ['$state', '$stateParams', 'searchData', function ($state, $stateParams, searchData) {
3 |
4 | var vm = this;
5 | vm.searchData = searchData;
6 | vm.currentRecipeName = $stateParams.recipeName;
7 |
8 | vm.switchPage = function () {
9 |
10 | var params = {
11 | id: $stateParams.id,
12 | page: vm.searchData.page,
13 | recipe: vm.currentRecipeName,
14 | pageSize: $stateParams.pageSize
15 | };
16 |
17 | $state.go('recipes.morelikethis', params);
18 | };
19 | }])
20 | })();
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Controllers/SearchController.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | app.controller('SearchController', ['$state', '$stateParams', 'searchResult', function ($state, $stateParams, searchResult) {
3 |
4 | var vm = this;
5 | vm.searchResult = searchResult;
6 |
7 | vm.switchPage = function () {
8 |
9 | var params = {
10 | query: $stateParams.query,
11 | page: vm.searchResult.page,
12 | pageSize: $stateParams.pageSize
13 | };
14 |
15 | $state.go('recipes.search', params);
16 | };
17 | }])
18 | })();
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Routes.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | app.config(['$locationProvider', '$stateProvider', function ($locationProvider, $stateProvider) {
3 |
4 | $stateProvider
5 | .state('default', {
6 | url: '/',
7 | redirectTo: 'recipes'
8 | });
9 |
10 | $stateProvider
11 | .state('recipes', {
12 | url: '/recipes',
13 | templateUrl: '/Views/Main.html',
14 | controller: 'MainController',
15 | controllerAs: 'model'
16 | });
17 |
18 | $stateProvider
19 | .state('recipes.search', {
20 | url: '/search/:query?page&pageSize',
21 | resolve: {
22 | searchResult: ['$q', 'RecipeService', '$stateParams', function ($q, RecipeService, $stateParams) {
23 |
24 | var deferred = $q.defer();
25 |
26 | RecipeService.getRecipes($stateParams.query, $stateParams.page, $stateParams.pageSize).then(function (response) {
27 | deferred.resolve(response.data);
28 | });
29 |
30 | return deferred.promise;
31 | }]
32 | },
33 | templateUrl: '/Views/SearchResult.html',
34 | controller: 'SearchController',
35 | controllerAs: 'model'
36 | });
37 |
38 | $stateProvider
39 | .state('recipes.details', {
40 | url: '/:id',
41 | resolve: {
42 | recipe: ['$q', 'RecipeService', '$stateParams', function ($q, RecipeService, $stateParams) {
43 |
44 | var deferred = $q.defer();
45 |
46 | RecipeService.getById($stateParams.id).then(function (response) {
47 | deferred.resolve(response.data);
48 | });
49 |
50 | return deferred.promise;
51 | }]
52 | },
53 | templateUrl: '/Views/Details.html',
54 | controller: 'DetailsController',
55 | controllerAs: 'model'
56 | });
57 |
58 | $stateProvider
59 | .state('recipes.morelikethis', {
60 | url: '/morelikethis/:id?page&pageSize&recipeName',
61 | resolve: {
62 | searchData: ['$q', 'RecipeService', '$stateParams', function ($q, RecipeService, $stateParams) {
63 |
64 | var deferred = $q.defer();
65 |
66 | if (typeof $stateParams.page !== "Number" || $stateParams.page <= 0) {
67 | $stateParams.page = 1;
68 | }
69 |
70 | RecipeService.moreLikeThis($stateParams.id, $stateParams.page, $stateParams.pageSize).then(function (response) {
71 | deferred.resolve(response.data);
72 | });
73 |
74 | return deferred.promise;
75 | }]
76 | },
77 | templateUrl: '/Views/MoreLikeThis.html',
78 | controller: 'MoreLikeThisController',
79 | controllerAs: 'model'
80 | });
81 |
82 | $locationProvider.html5Mode({
83 | enabled: true, requireBase: false
84 | });
85 | }]);
86 | })();
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Services/RecipeService.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | app.factory('RecipeService', ['$http', function ($http) {
3 |
4 | var recipesEndpoint = '/api/recipe/'
5 |
6 | var recipeService = {
7 |
8 | getRecipes: function (query, page, pageSize) {
9 |
10 | var route = recipesEndpoint + 'search';
11 |
12 | return $http.get(route, {
13 | params: {
14 | query: query,
15 | page: page,
16 | pageSize: pageSize
17 | }
18 | })
19 | },
20 | getById: function (id) {
21 | return $http.get(recipesEndpoint, {
22 | params: {
23 | id: id
24 | }
25 | });
26 | },
27 | moreLikeThis: function (id, page, pageSize) {
28 |
29 | var route = recipesEndpoint + 'morelikethis';
30 |
31 | return $http.get(route, {
32 | params: {
33 | id: id,
34 | page: page,
35 | pageSize: pageSize
36 | }
37 | })
38 | },
39 | autocomplete: function (query) {
40 |
41 | if (typeof query !== "undefined" && query.length > 0) {
42 | var route = recipesEndpoint + 'autocomplete';
43 |
44 | return $http.get(route, {
45 | params: {
46 | query: query
47 | }
48 | })
49 | }
50 | }
51 | }
52 |
53 | return recipeService;
54 | }]);
55 | })();
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Views/Details.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Description:
9 |
10 |
11 |
12 |
13 |
Ingredients:
14 |
15 |
16 |
17 |
18 |
19 | Cook time: {{ model.recipe.cookTime }},
20 |
21 |
22 | Prep time : {{ model.recipe.prepTime }},
23 |
24 |
25 | Yield: {{ model.recipe.recipeYield }}
26 |
27 |
28 |
29 | More like this recipe
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Views/Main.html:
--------------------------------------------------------------------------------
1 |
2 |
Elasticsearch Recipes
3 |
4 |
5 | Supported queries: wildcard (exampl*/exa?ple), must_not (-example), must (example), match_phrase ("example"), must_not match_phrase (-"example"), proximity (examlpe~1), boosting (example^2)
6 | and every other QueryStringQuery option.
7 |
8 |
9 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Views/MoreLikeThis.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Took: {{ model.searchData.elapsedMilliseconds }}ms, Page: {{ model.searchData.page }}, Results count: {{ model.searchData.total }}
4 |
5 |
6 |
Recipes like "{{ model.currentRecipeName }}"
7 |
8 |
9 |
10 |
11 | Id
12 | Name
13 | Description
14 | Ingredients
15 | Prep Time
16 | Link
17 | More like this
18 |
19 |
20 |
21 | {{ recipe.id }}
22 | {{ recipe.name }}
23 | {{ recipe.description }}
24 | {{ recipe.ingredients }}
25 | {{ recipe.prepTime }}
26 |
27 | Go to recipe
28 |
29 |
30 | More like this
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/Views/SearchResult.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 | Took: {{ model.searchResult.elapsedMilliseconds }}ms, Page: {{ model.searchResult.page }}, Results count: {{ model.searchResult.total }}
9 |
10 |
11 |
12 |
13 |
14 | Id
15 | Name
16 | Description
17 | Ingredients
18 | Prep Time
19 | Link
20 | More like this
21 |
22 |
23 |
24 | {{ recipe.id }}
25 | {{ recipe.name }}
26 | {{ recipe.description }}
27 | {{ recipe.ingredients }}
28 | {{ recipe.prepTime }}
29 |
30 | Go to recipe
31 |
32 |
33 | More like this
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/ElasticsearchRecipes/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Elasticsearch Recipes
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------