├── .gitignore ├── ElasticsearchFulltextExample.sln ├── LICENSE ├── README.md ├── archive ├── Angular │ └── search-app │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── angular.json │ │ ├── browserslist │ │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ │ ├── karma.conf.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.model.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ ├── document-status │ │ │ │ │ ├── document-status.component.html │ │ │ │ │ ├── document-status.component.scss │ │ │ │ │ └── document-status.component.ts │ │ │ │ ├── file-upload │ │ │ │ │ ├── file-upload.component.html │ │ │ │ │ ├── file-upload.component.scss │ │ │ │ │ └── file-upload.component.ts │ │ │ │ └── search │ │ │ │ │ ├── search.component.html │ │ │ │ │ ├── search.component.scss │ │ │ │ │ └── search.component.ts │ │ │ ├── services │ │ │ │ └── search.service.ts │ │ │ └── utils │ │ │ │ └── string-utils.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.spec.json │ │ └── tslint.json └── archive.md ├── doc └── img │ ├── ElasticsearchFulltextSearch_ManageSearchIndex.jpg │ ├── ElasticsearchFulltextSearch_Overview.jpg │ ├── ElasticsearchFulltextSearch_SearchResults.jpg │ ├── ElasticsearchFulltextSearch_Suggestions.jpg │ └── ElasticsearchFulltextSearch_Upload.jpg ├── docker-compose.yml ├── docker ├── .env ├── elasticsearch │ ├── elastic-cert │ │ ├── ca │ │ │ ├── ca.crt │ │ │ └── ca.key │ │ ├── es01.crt │ │ └── es01.key │ └── instances.yml ├── postgres │ └── postgres.conf ├── search-api │ └── Dockerfile └── search-web │ └── Dockerfile ├── sql ├── fts-data.sql ├── fts-replication.sql ├── fts-tests.sql ├── fts-versioning.sql └── fts.sql └── src ├── ElasticsearchFulltextExample.Api ├── Configuration │ ├── ApplicationOptions.cs │ ├── ElasticsearchOptions.cs │ ├── IndexerOptions.cs │ └── PostgresOutboxEventProcessorOptions.cs ├── Constants │ ├── Policies.cs │ ├── Roles.cs │ └── Users.cs ├── Controllers │ ├── ErrorController.cs │ └── SearchController.cs ├── ElasticsearchFulltextExample.Api.csproj ├── Hosting │ ├── ElasticsearchInitializerBackgroundService.cs │ └── PostgresOutboxEventProcessor.cs ├── Infrastructure │ ├── Authentication │ │ ├── ClaimsPrincipalExtensions.cs │ │ ├── CurrentUser.cs │ │ └── CurrentUserClaimsTransformation.cs │ ├── Elasticsearch │ │ ├── ElasticsearchConstants.cs │ │ ├── ElasticsearchSearchClient.cs │ │ ├── ElasticsearchSearchClientOptions.cs │ │ ├── Models │ │ │ ├── ElasticsearchAttachment.cs │ │ │ └── ElasticsearchDocument.cs │ │ └── Utils │ │ │ └── ElasticsearchUtils.cs │ ├── Errors │ │ ├── ApplicationErrorResult.cs │ │ ├── ErrorCodes.cs │ │ ├── ExceptionToApplicationErrorMapper.cs │ │ ├── ExceptionToApplicationErrorMapperOptions.cs │ │ ├── IExceptionTranslator.cs │ │ └── Translators │ │ │ ├── ApplicationErrorExceptionTranslator.cs │ │ │ ├── DefaultExceptionTranslator.cs │ │ │ └── InvalidModelStateExceptionTranslator.cs │ ├── Exceptions │ │ ├── ApplicationErrorException.cs │ │ ├── AuthenticationFailedException.cs │ │ ├── AuthorizationFailedException.cs │ │ ├── CannotDeleteOwnUserException.cs │ │ ├── EntityConcurrencyException.cs │ │ ├── EntityNotFoundException.cs │ │ └── InvalidModelStateException.cs │ ├── Mvc │ │ └── EnumRouteConstraint.cs │ └── Outbox │ │ ├── Consumer │ │ ├── IOutboxEventConsumer.cs │ │ └── OutboxEventConsumer.cs │ │ ├── Messages │ │ ├── DocumentCreatedMessage.cs │ │ ├── DocumentDeletedMessage.cs │ │ └── DocumentUpdatedMessage.cs │ │ ├── OutboxEventUtils.cs │ │ └── Postgres │ │ ├── PostgresOutboxSubscriber.cs │ │ └── PostgresOutboxSubscriberOptions.cs ├── Models │ ├── ApplicationError.cs │ ├── ApplicationErrorDetail.cs │ ├── ApplicationInnerError.cs │ ├── HighlightedContent.cs │ ├── SearchResult.cs │ ├── SearchResults.cs │ ├── SearchStatistics.cs │ ├── SearchSuggestion.cs │ └── SearchSuggestionsDto.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── DocumentService.cs │ ├── ElasticsearchService.cs │ └── UserService.cs ├── appsettings.Development.json ├── appsettings.Docker.json └── appsettings.json ├── ElasticsearchFulltextExample.Database ├── ApplicationDbContext.cs ├── ElasticsearchFulltextExample.Database.csproj ├── Model │ ├── Document.cs │ ├── DocumentKeyword.cs │ ├── DocumentSuggestion.cs │ ├── Entity.cs │ ├── Keyword.cs │ ├── OutboxEvent.cs │ ├── Suggestion.cs │ └── User.cs └── Properties │ └── launchSettings.json ├── ElasticsearchFulltextExample.Shared ├── Client │ ├── ApiException.cs │ └── SearchClient.cs ├── Constants │ ├── ElasticConstants.cs │ └── FileUploadNames.cs ├── ElasticsearchFulltextExample.Shared.csproj ├── Infrastructure │ └── LoggerExtensions.cs └── Models │ ├── SearchRequestDto.cs │ ├── SearchResultDto.cs │ ├── SearchResultsDto.cs │ ├── SearchStatisticsDto.cs │ ├── SearchSuggestionDto.cs │ └── SearchSuggestionsDto.cs ├── ElasticsearchFulltextExample.Web.Client ├── Components │ ├── Autocomplete │ │ ├── Autocomplete.razor │ │ ├── Autocomplete.razor.css │ │ ├── AutocompleteItem.cs │ │ └── AutocompleteSearchEventArgs.cs │ ├── NotificationCenter │ │ ├── NotificationCenter.razor │ │ └── NotificationCenterPanel.razor │ ├── Paginator │ │ ├── Paginator.razor │ │ ├── Paginator.razor.cs │ │ ├── Paginator.razor.css │ │ └── PaginatorState.cs │ ├── SearchResult │ │ ├── SearchResult.razor │ │ └── SearchResult.razor.css │ ├── SiteSettings │ │ ├── SiteSettings.razor │ │ ├── SiteSettings.razor.cs │ │ ├── SiteSettingsPanel.razor │ │ └── SiteSettingsPanel.razor.cs │ └── TokenInput │ │ ├── TokenInput.razor │ │ └── TokenInput.razor.css ├── ElasticsearchFulltextExample.Web.Client.csproj ├── Extensions │ ├── StringExtensions.cs │ └── StringLocalizerExtensions.cs ├── Infrastructure │ ├── ApplicationErrorMessageService.cs │ ├── ApplicationErrorTranslator.cs │ ├── DataSizeUtils.cs │ ├── EventCallbackSubscribable.cs │ ├── EventCallbackSubscriber.cs │ ├── SimpleValidator.cs │ ├── StringLocalizerExtensions.cs │ └── TimeFormattingUtils.cs ├── Localization │ ├── LocalizationConstants.cs │ ├── SharedResource.cs │ └── SharedResource.resx ├── Models │ ├── ElasticsearchIndexMetrics.cs │ ├── ElasticsearchMetric.cs │ └── SortOptionEnum.cs ├── Pages │ ├── Index.razor │ ├── Index.razor.cs │ ├── Index.razor.css │ ├── Manage.razor │ ├── Manage.razor.cs │ ├── Search.razor │ ├── Search.razor.cs │ ├── Search.razor.css │ ├── Upload.razor │ ├── Upload.razor.cs │ └── Upload.razor.css ├── Program.cs ├── Properties │ └── launchSettings.json ├── Routes.razor ├── Routes.razor.cs ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.cs │ ├── MainLayout.razor.js │ └── NavMenu.razor ├── _Imports.razor └── wwwroot │ ├── appsettings.json │ └── images │ ├── 512 │ ├── HTML5_logo_and_wordmark.svg.png │ ├── PDF_file_icon.png │ ├── Text-txt.svg.png │ └── docx_icon.svg.png │ ├── HTML5_logo_and_wordmark.svg │ ├── JPEG_format_logo.svg │ ├── Markdown-mark.svg │ ├── PDF_file_icon.svg │ ├── Text-txt.svg │ └── docx_icon.svg └── ElasticsearchFulltextExample.Web.Server ├── App.razor ├── ElasticsearchFulltextExample.Web.Server.csproj ├── Program.cs ├── Properties └── launchSettings.json ├── _Imports.razor ├── appsettings.Development.json ├── appsettings.json └── wwwroot ├── css └── app.css ├── favicon.ico ├── icon-192.png ├── img ├── extension-html.png ├── extension-jpeg.png ├── extension-pdf.png ├── extension-svg.png ├── extension-txt.png └── extensions-md.png └── js └── theme.js /ElasticsearchFulltextExample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticsearchFulltextExample.Api", "src\ElasticsearchFulltextExample.Api\ElasticsearchFulltextExample.Api.csproj", "{F2632FB9-21F8-47DA-B04E-3FC39CE514AC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticsearchFulltextExample.Shared", "src\ElasticsearchFulltextExample.Shared\ElasticsearchFulltextExample.Shared.csproj", "{291FDB9B-B93F-4964-BF4D-7B46928DA4BE}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticsearchFulltextExample.Web.Client", "src\ElasticsearchFulltextExample.Web.Client\ElasticsearchFulltextExample.Web.Client.csproj", "{757702B0-EE3C-4919-8AC4-9A919816EEED}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticsearchFulltextExample.Database", "src\ElasticsearchFulltextExample.Database\ElasticsearchFulltextExample.Database.csproj", "{5F9FA1DA-A186-4495-8BE0-972EA624F53A}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElasticsearchFulltextExample.Web.Server", "src\ElasticsearchFulltextExample.Web.Server\ElasticsearchFulltextExample.Web.Server.csproj", "{C1EB93D4-20AC-4635-A7B6-1454E7350787}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {F2632FB9-21F8-47DA-B04E-3FC39CE514AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F2632FB9-21F8-47DA-B04E-3FC39CE514AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F2632FB9-21F8-47DA-B04E-3FC39CE514AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {F2632FB9-21F8-47DA-B04E-3FC39CE514AC}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {291FDB9B-B93F-4964-BF4D-7B46928DA4BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {291FDB9B-B93F-4964-BF4D-7B46928DA4BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {291FDB9B-B93F-4964-BF4D-7B46928DA4BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {291FDB9B-B93F-4964-BF4D-7B46928DA4BE}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {757702B0-EE3C-4919-8AC4-9A919816EEED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {757702B0-EE3C-4919-8AC4-9A919816EEED}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {757702B0-EE3C-4919-8AC4-9A919816EEED}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {757702B0-EE3C-4919-8AC4-9A919816EEED}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {5F9FA1DA-A186-4495-8BE0-972EA624F53A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {5F9FA1DA-A186-4495-8BE0-972EA624F53A}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {5F9FA1DA-A186-4495-8BE0-972EA624F53A}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {5F9FA1DA-A186-4495-8BE0-972EA624F53A}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {C1EB93D4-20AC-4635-A7B6-1454E7350787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {C1EB93D4-20AC-4635-A7B6-1454E7350787}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {C1EB93D4-20AC-4635-A7B6-1454E7350787}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {C1EB93D4-20AC-4635-A7B6-1454E7350787}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {85104025-333E-4C05-8CC8-5DD6BB619F0E} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Philipp Wagner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elasticsearch Fulltext Search Example # 2 | 3 | This repository implements a Fulltext Search Engine using ASP.NET Core, PostgreSQL and Elasticsearch. You can 4 | use it to index documents, such as PDF, Microsoft Word, Markdown or HTML. It comes with a Blazor Frontend built 5 | upon the FluentUI Component library. 6 | 7 | If you are looking for the old Angular version, use the [angular](https://github.com/bytefish/ElasticsearchFulltextExample/tags) tag. 8 | 9 | ## What's included ## 10 | 11 | There is a page for searching documents and displaying the matches. The search results highlight the matching text: 12 | 13 | 14 | The final Document Search with the Blazor Frontend 15 | 16 | 17 | We are using suggestions to give it a Google-like experience and make it easier to find documents by Keywords: 18 | 19 | 20 | Search Suggestions 21 | 22 | 23 | There is also a page to upload documents and assign Keywords, using a Token Input: 24 | 25 | 26 | Document Upload 27 | 28 | 29 | For debugging there is also an Overview to summarize the current state of the Elasticsearch index: 30 | 31 | 32 | Document Upload 33 | 34 | 35 | ## Getting Started ## 36 | 37 | Getting started is as simple as cloning this repository and running the following command: 38 | 39 | ``` 40 | docker compose --profile dev up 41 | ``` 42 | 43 | You can then navigate to `https://localhost:5001` and start searching and indexing documents. 44 | 45 | ## License ## 46 | 47 | All code is released under terms of the [MIT License]. 48 | 49 | [MIT License]: https://opensource.org/licenses/MIT 50 | -------------------------------------------------------------------------------- /archive/Angular/search-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /archive/Angular/search-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /archive/Angular/search-app/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /archive/Angular/search-app/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /archive/Angular/search-app/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('search-app app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /archive/Angular/search-app/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /archive/Angular/search-app/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /archive/Angular/search-app/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/search-app'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /archive/Angular/search-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~9.0.2", 15 | "@angular/cdk": "^9.2.0", 16 | "@angular/common": "~9.0.2", 17 | "@angular/compiler": "~9.0.2", 18 | "@angular/core": "~9.0.2", 19 | "@angular/flex-layout": "^9.0.0-beta.29", 20 | "@angular/forms": "~9.0.2", 21 | "@angular/material": "^9.2.0", 22 | "@angular/platform-browser": "~9.0.2", 23 | "@angular/platform-browser-dynamic": "~9.0.2", 24 | "@angular/router": "~9.0.2", 25 | "rxjs": "~6.5.4", 26 | "tslib": "^1.10.0", 27 | "zone.js": "~0.10.2" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~0.900.3", 31 | "@angular/cli": "~9.0.3", 32 | "@angular/compiler-cli": "~9.0.2", 33 | "@angular/language-service": "~9.0.2", 34 | "@types/node": "^12.11.1", 35 | "@types/jasmine": "~3.5.0", 36 | "@types/jasminewd2": "~2.0.3", 37 | "codelyzer": "^5.1.2", 38 | "jasmine-core": "~3.5.0", 39 | "jasmine-spec-reporter": "~4.2.1", 40 | "karma": "~4.3.0", 41 | "karma-chrome-launcher": "~3.1.0", 42 | "karma-coverage-istanbul-reporter": "~2.1.0", 43 | "karma-jasmine": "~2.0.1", 44 | "karma-jasmine-html-reporter": "^1.4.2", 45 | "protractor": "~5.4.3", 46 | "ts-node": "~8.3.0", 47 | "tslint": "~5.18.0", 48 | "typescript": "~3.7.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | import { NgModule } from '@angular/core'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | import { SearchComponent } from '@app/components/search/search.component'; 7 | import { DocumentStatusComponent } from './components/document-status/document-status.component'; 8 | 9 | 10 | const routes: Routes = [ 11 | { path: '', 12 | pathMatch: 'full', 13 | redirectTo: "search" 14 | }, 15 | { 16 | path: 'search', 17 | component: SearchComponent 18 | }, 19 | { 20 | path: 'status', 21 | component: DocumentStatusComponent 22 | } 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forRoot(routes)], 27 | exports: [RouterModule] 28 | }) 29 | export class AppRoutingModule { } 30 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 25 |
26 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | @import '~@angular/material/theming'; 5 | 6 | $accent: mat-palette($mat-amber); 7 | 8 | .search-container { 9 | height: auto; 10 | } 11 | 12 | .search-bar { 13 | height: 60px; 14 | background-color: mat-color($accent, 200); 15 | box-shadow: 0 1px 2px rgba(0,0,0,0.05),0 1px 4px rgba(0,0,0,0.05),0 2px 8px rgba(0,0,0,0.05); 16 | } 17 | 18 | input { 19 | border: solid 1px black; 20 | outline: none; 21 | margin: 10px; 22 | padding: 6px 16px; 23 | width: 100%; 24 | max-width: 600px; 25 | height: 40px; 26 | font-size: 16px; 27 | } 28 | 29 | .add-button { 30 | position: fixed; 31 | top: auto; 32 | right: 30px; 33 | bottom: 30px; 34 | left: auto; 35 | } -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | import { Component, ViewChild } from '@angular/core'; 5 | import { SearchSuggestions } from '@app/app.model'; 6 | import { HttpClient } from '@angular/common/http'; 7 | import { environment } from '@environments/environment'; 8 | import { Router, ActivatedRoute } from '@angular/router'; 9 | import { Observable, of } from 'rxjs'; 10 | import { switchMap, debounceTime, catchError, map, filter } from 'rxjs/operators'; 11 | import { FormControl } from '@angular/forms'; 12 | import { MatDialog } from '@angular/material/dialog'; 13 | import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; 14 | import { FileUploadComponent } from './components/file-upload/file-upload.component'; 15 | import { DocumentStatusComponent } from './components/document-status/document-status.component'; 16 | import { SearchService } from './services/search.service'; 17 | 18 | @Component({ 19 | selector: 'app-root', 20 | templateUrl: './app.component.html', 21 | styleUrls: ['./app.component.scss'] 22 | }) 23 | export class AppComponent { 24 | 25 | destroy$: Observable; 26 | 27 | control = new FormControl(); 28 | 29 | query$: Observable; 30 | suggestions$: Observable; 31 | 32 | @ViewChild('search', { read: MatAutocompleteTrigger }) 33 | autoComplete: MatAutocompleteTrigger; 34 | 35 | constructor(private route: ActivatedRoute, 36 | private searchService: SearchService, 37 | private dialog: MatDialog, 38 | private router: Router, 39 | private httpClient: HttpClient) { 40 | 41 | } 42 | 43 | ngOnInit(): void { 44 | 45 | this.route.queryParams 46 | .pipe( 47 | map(params => params['q']), 48 | filter(query => !!query) 49 | ) 50 | .subscribe(query => { 51 | this.control.setValue(query); 52 | this.searchService.submitSearch(query); 53 | }); 54 | 55 | this.suggestions$ = this.control.valueChanges 56 | .pipe( 57 | debounceTime(300), // Debounce time to not send every keystroke ... 58 | switchMap(value => this 59 | .getSuggestions(value) 60 | .pipe(catchError(() => of({ query: value, results: []})))) 61 | ); 62 | } 63 | 64 | onKeyupEnter(value: string): void { 65 | 66 | if(!!this.autoComplete) { 67 | this.autoComplete.closePanel(); 68 | } 69 | 70 | // Instead of firing the Search directly, let's update the Route instead: 71 | this.router.navigate(['/search'], { queryParams: { q: value } }); 72 | } 73 | 74 | getSuggestions(query: string): Observable { 75 | 76 | if (!query) { 77 | return of(null); 78 | } 79 | 80 | return this.httpClient 81 | // Get the Results from the API: 82 | .get(`${environment.apiUrl}/suggest`, { 83 | params: { 84 | q: query 85 | } 86 | }) 87 | .pipe(catchError((err) => { 88 | console.error(`An error occured while fetching suggestions: ${err}`); 89 | 90 | return of({ query: query, results: []}) 91 | })); 92 | } 93 | 94 | openFileUploadDialog() { 95 | this.dialog.open(FileUploadComponent); 96 | } 97 | 98 | openDocumentStatusDialog() { 99 | this.dialog.open(DocumentStatusComponent); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/app.model.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | export enum SearchStateEnum { 5 | Loading = "loading", 6 | Finished = "finished", 7 | Error = "error" 8 | } 9 | 10 | export interface SearchQuery { 11 | state: SearchStateEnum; 12 | data: SearchResults; 13 | error: string; 14 | } 15 | 16 | export interface SearchResults { 17 | query: string; 18 | results: SearchResult[]; 19 | } 20 | 21 | export interface SearchResult { 22 | identifier: string; 23 | title: string; 24 | matches: string[]; 25 | keywords: string[]; 26 | url: string; 27 | type: string; 28 | } 29 | 30 | export interface SearchSuggestions { 31 | query: string; 32 | results: SearchSuggestion[]; 33 | } 34 | 35 | export interface SearchSuggestion { 36 | text: string; 37 | highlight: string; 38 | } 39 | 40 | export enum StatusEnum { 41 | None = "none", 42 | ScheduledIndex = "scheduledIndex", 43 | ScheduledDelete = "scheduledDelete", 44 | Indexed = "indexed", 45 | Failed = "failed", 46 | Deleted = "deleted" 47 | } 48 | 49 | export interface DocumentStatus { 50 | id: number; 51 | filename: string; 52 | title: string; 53 | isOcrRequested: boolean; 54 | status: StatusEnum; 55 | } -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | import { HttpClientModule } from '@angular/common/http'; 7 | import { NgModule } from '@angular/core'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { AppComponent } from './app.component'; 10 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 11 | import { FlexLayoutModule } from '@angular/flex-layout'; 12 | import { MatDialogModule } from '@angular/material/dialog'; 13 | import { MatInputModule } from '@angular/material/input'; 14 | import { MatButtonModule } from '@angular/material/button'; 15 | import { MatChipsModule } from '@angular/material/chips'; 16 | import { MatIconModule } from '@angular/material/icon'; 17 | import { MatCardModule } from '@angular/material/card'; 18 | import { MatFormFieldModule } from '@angular/material/form-field'; 19 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 20 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 21 | import { MatCheckboxModule } from '@angular/material/checkbox'; 22 | import { MatTableModule } from '@angular/material/table'; 23 | import { MatMenuModule } from '@angular/material/menu'; 24 | import { FileUploadComponent } from '@app/components/file-upload/file-upload.component'; 25 | import { SearchComponent } from '@app/components/search/search.component'; 26 | import { DocumentStatusComponent } from '@app/components/document-status/document-status.component'; 27 | import { SearchService } from './services/search.service'; 28 | 29 | @NgModule({ 30 | declarations: [ 31 | AppComponent, 32 | SearchComponent, 33 | FileUploadComponent, 34 | DocumentStatusComponent 35 | ], 36 | imports: [ 37 | BrowserModule, 38 | HttpClientModule, 39 | AppRoutingModule, 40 | BrowserAnimationsModule, 41 | ReactiveFormsModule, 42 | FormsModule, 43 | FlexLayoutModule, 44 | MatInputModule, 45 | MatCardModule, 46 | MatFormFieldModule, 47 | MatAutocompleteModule, 48 | MatDialogModule, 49 | MatProgressSpinnerModule, 50 | MatButtonModule, 51 | MatIconModule, 52 | MatChipsModule, 53 | MatCheckboxModule, 54 | MatTableModule, 55 | MatMenuModule 56 | ], 57 | providers: [ 58 | SearchService 59 | ], 60 | bootstrap: [AppComponent] 61 | }) 62 | export class AppModule { } 63 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/document-status/document-status.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
10 | 13 | 14 | 16 | 19 | 20 | Document ID {{element.id}} Title {{element.title}} Filename {{element.filename}} Additional OCR 37 | 39 | Status {{element.status}}
49 |
50 |
51 |
52 | 53 | 54 |
55 |
-------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/document-status/document-status.component.scss: -------------------------------------------------------------------------------- 1 | .document-status-container { 2 | margin: 25px; 3 | } 4 | 5 | .mat-form-field-padding { 6 | margin: 15px; 7 | } 8 | 9 | .min-chips-height { 10 | min-height: 50px; 11 | } 12 | 13 | table { 14 | width: 100%; 15 | } 16 | 17 | td.mat-column-select { 18 | width: 50px; 19 | } 20 | 21 | td.mat-column-documentId { 22 | width: 300px; 23 | } 24 | 25 | td.mat-column-filename { 26 | width: 400px; 27 | } 28 | 29 | td.mat-column-isOcrRequested { 30 | width: 100px; 31 | } 32 | 33 | td.mat-column-status { 34 | width: 150px; 35 | } 36 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/file-upload/file-upload.component.html: -------------------------------------------------------------------------------- 1 |

Add a Document to the Search Index

2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | {{suggestion}} 15 | cancel 16 | 17 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 | 31 | 32 | 35 |
36 |
37 | 38 |
39 | Add OCR Data to Search Index 40 |
41 |
42 |
43 | 44 |
45 |
46 |
-------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/file-upload/file-upload.component.scss: -------------------------------------------------------------------------------- 1 | .file-input-container { 2 | width: 500px; 3 | margin: 25px; 4 | } 5 | 6 | .mat-form-field-padding { 7 | margin: 15px; 8 | } 9 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/search/search.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 | 10 | 11 |
12 |

We are very sorry... There was an error processing the request. Maybe try again later? 😓

13 |
14 |
15 | 16 | 17 |
18 |

This query has no results. Maybe try a different one? 😓

19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 |
27 |

{{result.title}}

28 |
29 |
30 |
31 |

Matches in Content:

32 |
    33 |
  • 34 |
35 |
36 |
37 | 38 | {{keyword}} 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
-------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/search/search.component.scss: -------------------------------------------------------------------------------- 1 | .search-result { 2 | width: 600px; 3 | } 4 | 5 | .search-results { 6 | background-color: #eee; 7 | height: 100%; 8 | padding: 25px; 9 | } 10 | 11 | .search-link { 12 | 13 | color: rgb(2, 80, 224); 14 | text-decoration: none; 15 | 16 | &:visited { 17 | color: rgb(2, 80, 224); 18 | } 19 | } 20 | 21 | h3 { 22 | margin: 0; 23 | font-size: 20px; 24 | line-height: 1.3; 25 | } 26 | 27 | .mat-card-content { 28 | margin: 0; 29 | word-wrap: break-word; 30 | } 31 | 32 | p { 33 | margin: 0; 34 | } -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/components/search/search.component.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | import { Component, OnInit, OnDestroy } from '@angular/core'; 5 | import { SearchResults, SearchStateEnum, SearchQuery } from '@app/app.model'; 6 | import { HttpClient } from '@angular/common/http'; 7 | import { environment } from '@environments/environment'; 8 | import { Observable, of, concat, Subject } from 'rxjs'; 9 | import { map, switchMap, filter, catchError, takeUntil } from 'rxjs/operators'; 10 | import { FormControl } from '@angular/forms'; 11 | import { SearchService } from '@app/services/search.service'; 12 | 13 | @Component({ 14 | selector: 'app-search', 15 | templateUrl: './search.component.html', 16 | styleUrls: ['./search.component.scss'] 17 | }) 18 | export class SearchComponent implements OnInit, OnDestroy { 19 | 20 | destroy$ = new Subject(); 21 | 22 | control = new FormControl(); 23 | query$: Observable; 24 | 25 | constructor(private httpClient: HttpClient, private searchService: SearchService) { 26 | 27 | } 28 | 29 | ngOnInit(): void { 30 | this.query$ = this.searchService.onSearchSubmit() 31 | .pipe( 32 | filter(query => !!query.term), 33 | switchMap(query => 34 | concat( 35 | of({ state: SearchStateEnum.Loading }), 36 | this.doSearch(query.term).pipe( 37 | map(results => {state: SearchStateEnum.Finished, data: results}), 38 | catchError(err => of({ state: SearchStateEnum.Error, error: err })) 39 | ) 40 | ) 41 | ), 42 | takeUntil(this.destroy$) 43 | ); 44 | } 45 | 46 | doSearch(query: string): Observable { 47 | return this.httpClient 48 | .get(`${environment.apiUrl}/search`, { 49 | params: { 50 | q: query 51 | } 52 | }); 53 | } 54 | 55 | ngOnDestroy() { 56 | this.destroy$.next(); 57 | this.destroy$.complete(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/services/search.service.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | import { Injectable } from '@angular/core'; 5 | import { Subject, Observable, BehaviorSubject } from 'rxjs'; 6 | import { share } from 'rxjs/operators'; 7 | 8 | @Injectable() 9 | export class SearchService { 10 | 11 | private searchSubmittings$ = new BehaviorSubject<{ term: string }>({ term: null }); 12 | 13 | submitSearch(term: string) { 14 | this.searchSubmittings$.next({ term }); 15 | } 16 | 17 | onSearchSubmit(): Observable<{ term: string }> { 18 | return this.searchSubmittings$.pipe(share()); 19 | } 20 | } -------------------------------------------------------------------------------- /archive/Angular/search-app/src/app/utils/string-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | export class StringUtils { 5 | 6 | static isNullOrWhitespace(input: string): boolean { 7 | return !input || !input.trim(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/archive/Angular/search-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /archive/Angular/search-app/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | export const environment = { 5 | production: true, 6 | apiUrl: "http://localhost:9000/api" 7 | }; 8 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | export const environment = { 5 | production: false, 6 | apiUrl: "http://localhost:9000/api" 7 | }; 8 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/archive/Angular/search-app/src/favicon.ico -------------------------------------------------------------------------------- /archive/Angular/search-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SearchApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | import { enableProdMode } from '@angular/core'; 5 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 6 | 7 | import { AppModule } from './app/app.module'; 8 | import { environment } from './environments/environment'; 9 | 10 | if (environment.production) { 11 | enableProdMode(); 12 | } 13 | 14 | platformBrowserDynamic().bootstrapModule(AppModule) 15 | .catch(err => console.error(err)); 16 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /archive/Angular/search-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | html, body 2 | { 3 | box-sizing: border-box; 4 | height: 100%; 5 | } 6 | 7 | body 8 | { 9 | margin: 0; 10 | background-color: #eee; 11 | } -------------------------------------------------------------------------------- /archive/Angular/search-app/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /archive/Angular/search-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /archive/Angular/search-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "@app/*": [ 23 | "src/app/*" 24 | ], 25 | "@environments/*": [ 26 | "src/environments/*" 27 | ] 28 | } 29 | }, 30 | "angularCompilerOptions": { 31 | "fullTemplateTypeCheck": true, 32 | "strictInjectionParameters": true 33 | } 34 | } -------------------------------------------------------------------------------- /archive/Angular/search-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /archive/Angular/search-app/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-var-requires": false, 64 | "object-literal-key-quotes": [ 65 | true, 66 | "as-needed" 67 | ], 68 | "object-literal-sort-keys": false, 69 | "ordered-imports": false, 70 | "quotemark": [ 71 | true, 72 | "single" 73 | ], 74 | "trailing-comma": false, 75 | "no-conflicting-lifecycle": true, 76 | "no-host-metadata-property": true, 77 | "no-input-rename": true, 78 | "no-inputs-metadata-property": true, 79 | "no-output-native": true, 80 | "no-output-on-prefix": true, 81 | "no-output-rename": true, 82 | "no-outputs-metadata-property": true, 83 | "template-banana-in-box": true, 84 | "template-no-negated-async": true, 85 | "use-lifecycle-interface": true, 86 | "use-pipe-transform-interface": true 87 | }, 88 | "rulesDirectory": [ 89 | "codelyzer" 90 | ] 91 | } -------------------------------------------------------------------------------- /archive/archive.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/archive/archive.md -------------------------------------------------------------------------------- /doc/img/ElasticsearchFulltextSearch_ManageSearchIndex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/doc/img/ElasticsearchFulltextSearch_ManageSearchIndex.jpg -------------------------------------------------------------------------------- /doc/img/ElasticsearchFulltextSearch_Overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/doc/img/ElasticsearchFulltextSearch_Overview.jpg -------------------------------------------------------------------------------- /doc/img/ElasticsearchFulltextSearch_SearchResults.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/doc/img/ElasticsearchFulltextSearch_SearchResults.jpg -------------------------------------------------------------------------------- /doc/img/ElasticsearchFulltextSearch_Suggestions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/doc/img/ElasticsearchFulltextSearch_Suggestions.jpg -------------------------------------------------------------------------------- /doc/img/ElasticsearchFulltextSearch_Upload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/doc/img/ElasticsearchFulltextSearch_Upload.jpg -------------------------------------------------------------------------------- /docker/.env: -------------------------------------------------------------------------------- 1 | ELASTIC_HOSTNAME=es01 2 | ELASTIC_USERNAME=elastic 3 | ELASTIC_PASSWORD=secret 4 | ELASTIC_PORT=9200 5 | ELASTIC_SECURITY=true 6 | ELASTIC_SCHEME=https 7 | ELASTIC_VERSION=8.14.1 8 | -------------------------------------------------------------------------------- /docker/elasticsearch/elastic-cert/ca/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDSjCCAjKgAwIBAgIVAIY8PVx5b95idpng6xwVrgvMtH9CMA0GCSqGSIb3DQEB 3 | CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu 4 | ZXJhdGVkIENBMB4XDTI0MDYyMDAyNTg0OVoXDTI3MDYyMDAyNTg0OVowNDEyMDAG 5 | A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew 6 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpIPQlIgJYjCUE72lrTl8o 7 | APRDqM3G10kOKFJpnzIx7NtU3Fq41snc8dM5xbn0WgcTfceAkxP/TkacuaDd/wMV 8 | XebHlx/K1/VAXOXWVE5DB0Bc+1TybE2B225hCVOYHwDXD0TzvfFfkUXDtzCYPvYx 9 | 0UwnCeq/loGuTlHZE7oZY8hOoS+NctiTyxAgGPKU6QI41st1M9Oyd/mEg0mSXlfu 10 | 6vsNNwzPbMmiMuIrOnmqWLJNJ9JMJxjPTicuzPKzZLFgjcZbg9JsCl+xzF44ULTT 11 | ITAjfmZ0lfwBSydi90bn7tdBgMPYi3GpdBMAE2fKm6Fwj8hePcBnCHADC46dgcGN 12 | AgMBAAGjUzBRMB0GA1UdDgQWBBSYquwhYddRUdbByLo9rURPNYiLVTAfBgNVHSME 13 | GDAWgBSYquwhYddRUdbByLo9rURPNYiLVTAPBgNVHRMBAf8EBTADAQH/MA0GCSqG 14 | SIb3DQEBCwUAA4IBAQCBs/+RubVqtSSyJCjcnBhy4OLwajLORREs2Bdl8/rmFBfg 15 | gxf+sJvnGedPjYJnr37xUJnyCw+pNPYNpPIkaI6Y8FGXg8In8wxqNter5OlVpxBw 16 | kD3m8396QAYNAdLwka7KyQGmilvb2LASwkMp6WSdo6MFiuV58P+ynjhFXZ0d7ybQ 17 | 6DmnsteZ1Omw2ERq4xBVFWq4EmS4BUqqR9GwS7ibKCzA6qZD+il8tKBAGwa67t23 18 | I1z43+oSLCtBzJuWREzt8G7BcFTy63PeZvyhnKnwsAvPdqKs3cvWhIkdCmdIniD5 19 | 5ULVwWPLU/ChAOhrtBkwYGCqjRsGtbDVJwc82lPC 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /docker/elasticsearch/elastic-cert/ca/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAqSD0JSICWIwlBO9pa05fKAD0Q6jNxtdJDihSaZ8yMezbVNxa 3 | uNbJ3PHTOcW59FoHE33HgJMT/05GnLmg3f8DFV3mx5cfytf1QFzl1lROQwdAXPtU 4 | 8mxNgdtuYQlTmB8A1w9E873xX5FFw7cwmD72MdFMJwnqv5aBrk5R2RO6GWPITqEv 5 | jXLYk8sQIBjylOkCONbLdTPTsnf5hINJkl5X7ur7DTcMz2zJojLiKzp5qliyTSfS 6 | TCcYz04nLszys2SxYI3GW4PSbApfscxeOFC00yEwI35mdJX8AUsnYvdG5+7XQYDD 7 | 2ItxqXQTABNnypuhcI/IXj3AZwhwAwuOnYHBjQIDAQABAoIBAEe1dC7GU65NhWip 8 | Rc48hXYFqYuCZ/U11IDPMdocqICoh3pcj46taxtl4QQuxKBJB5UJEGyAb8sg2imb 9 | PwzBEgKeNLpNZipwFEk82ipcxm3/BhgmbCb5KoezjQJRnQLzqjyE+dxKnavCgYzx 10 | AAadM2996UboGoMvAj7wcB2VEqOujYeUHo79UTe4XJEMSy+qY7DXXjGH12v5+oHv 11 | ZE9HshlIxkmhiBnTKhMMz7iFmspf9JLmWqHTL43Gh6AGDbQAe++nBHlYESaJi4aE 12 | SPGSIT6/A+MGbgi109BmoFyb9Yl/VUCHoITE/+EEexfwQ2+rDFXlzsC7sq93PMUu 13 | NlwCTbUCgYEAzMpa0S6b1fb/adJf5uqUzANt5uyvAX7I8V5pX8BEvvQf8XwSfn9s 14 | BtG/vR/1wKog13r1QdQu01nBUPM+m7g7o6OUTzYYpRpCNGxJJQDTuICsZxebtCRg 15 | HHggeyDEUhbF4hHW0wXz7R9ZYzCw7vMsSst9EuOXadZhrWOtIlFvRF8CgYEA02u3 16 | ZqSHGGHfl87Q2951rdiD3i1iVAqi6ZAKsPrZSapm87ETlpBjyFlpEV8OoW8wviuQ 17 | T3Epwn9hLEt6m7DRxVUZV02LlWbWFtYvZmFCT2MaASAPF9kNMXbiWCIPtM0imAtr 18 | 1fjzJMeuI4xk4eNceGbK0tOYt7IRRNWbB6464ZMCgYBIvT9QuYtkjlzeS3kA3iWH 19 | 6VfqA/uNPmlFQlGPTw2b/b4y2ez/vWazbWD7XhS+IC+WGfhvL1yKeYDurdd5HBEi 20 | 6IFPOVm7mv4U/LlmSUrqZ9nUrFADxj/VHN38ngDdX3Vd/RQe1Ch1+wKW9r6BwSHk 21 | Vy7PvMMVNq5vFc5zOBWbZQKBgQC1WFGp8zoFyf3V4wn3biuWgH8r1dXfrHfsyybA 22 | g4pZy5YfNSZOIW/VbAvZYKXWBt/SXt+bpL9jG4uoSN3UKajlEId6AQPXlKvSTsm9 23 | kXMlUSX/DRalKAJPCWBApIbAWKxxqmpG0REN3VEbINNvhmvMwq76g5EdD9oMJwSh 24 | Y+sRjQKBgQCwXeTwvbSITkCd5vSMxQg1RNVnLdP+LyJ8NunJlZt+mQI6/m4S0bKA 25 | d5w9DoaHIKIz/BwP1ERclFgzRY4wA8jOGE9NS+x7qQbvgN3lLlii/ySUHDaX2FhD 26 | rAj1lkUTPVlYmTnNdChe8G3AMeVHMJk4+AVVy7SFpotICV3XHqZEuw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docker/elasticsearch/elastic-cert/es01.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQTCCAimgAwIBAgIVAO6ERxm6MOvaRt4VyG29RYi6c+U0MA0GCSqGSIb3DQEB 3 | CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu 4 | ZXJhdGVkIENBMB4XDTI0MDYyMDAzMjMxNFoXDTI3MDYyMDAzMjMxNFowDzENMAsG 5 | A1UEAxMEZXMwMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFp/uru 6 | DNQ8HQ22S4uqYZd2YB9uWry61R/afytpGzJdGjmeqjWBF6oesLQr9yffLet/UgG4 7 | DbA6nrhQDFQzzS218fMjsM08hsCeU3lk1J3OUKMCiugKiO61tG/wWbi9/YJ3pQtg 8 | kXnNt4uK74ELmrmXMvs+BHuJzuexOMRqnHvntA5QhilpEIWRoVAL1QxYXWC/lo8u 9 | Ketl8w0yn/RB1InWUftlKiaiooq3M57/yp+5Es+xVvCnSD99eL/oX2+JRMKgi4Zy 10 | EA7Bowem7cVIefDC6VRtAsuUzfqsAiSEOXbTHrnk1XyBWVXlBBtrJ0XiEPfPTH9E 11 | VVS2lpo4HsqTXoUCAwEAAaNvMG0wHQYDVR0OBBYEFAL5QpafWzfww2hgsoih+8rH 12 | iNFxMB8GA1UdIwQYMBaAFJiq7CFh11FR1sHIuj2tRE81iItVMCAGA1UdEQQZMBeC 13 | BGVzMDGHBH8AAAGCCWxvY2FsaG9zdDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA 14 | A4IBAQBcH5jj8B5JY5xA+iSqzB669cw6iEWVWSV9GJ5i6nMtVVFE4rV/iUhxrpd/ 15 | k2u3wbBBTZEtkMNv9jmeUOUrohIWufgNUgdD5obJtT1Zgw1FFV4EUjbEn6EjDqxo 16 | S93pm2qK3qHeZtwYOzbZ+6Mal1gLRZ8B2kBzAkUdLBDJ+EMP2vuX13qakwt43TFW 17 | qqi4goLDa92jBGI5bqW2FJ+w8h5junIJItvSdbXKL1yYDfjFMeqdHMBYX8bE/Opl 18 | XfgALMfZgoq+nRFkLm3wDTa4T/ib+CysGXCABwshqGplUJxOcUQWG8xB0dtdXje9 19 | W0wNRjS/ppWK2tv98K0jok2iNXLN 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /docker/elasticsearch/elastic-cert/es01.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAsWn+6u4M1DwdDbZLi6phl3ZgH25avLrVH9p/K2kbMl0aOZ6q 3 | NYEXqh6wtCv3J98t639SAbgNsDqeuFAMVDPNLbXx8yOwzTyGwJ5TeWTUnc5QowKK 4 | 6AqI7rW0b/BZuL39gnelC2CRec23i4rvgQuauZcy+z4Ee4nO57E4xGqce+e0DlCG 5 | KWkQhZGhUAvVDFhdYL+Wjy4p62XzDTKf9EHUidZR+2UqJqKiircznv/Kn7kSz7FW 6 | 8KdIP314v+hfb4lEwqCLhnIQDsGjB6btxUh58MLpVG0Cy5TN+qwCJIQ5dtMeueTV 7 | fIFZVeUEG2snReIQ989Mf0RVVLaWmjgeypNehQIDAQABAoIBAAeI7rh//byx0bLe 8 | ycL+LIDoGFlqMKGQkDN3IZMj0D58vi7+S+pT0XNHNGgI8GLmhqbyB5CZ7L+0VAz6 9 | t8G68koW6FZUO+xFJGmTD9m5bF8U/4KJpBtwy+bStTDsroYH6hFoqKWc2op0Yzw8 10 | KinhgH1eksRUgPcIn+hiALA1R71Q95sulKrWMFhKh1Qc+MNUQ3p0GqNEeg2g+fTv 11 | +MTs6DQtcnkenZoribwuWckJs/myA4veAA/H+tFFS5YuQ91Wn0aomUXCskqhYTul 12 | Hwr0U+tzwvs+wmZP4l8Y+CrvSoj4F0cKfmM1NXBcQp5WFJL/YtODoA8fATcNk7Jd 13 | M5uCfG0CgYEA2pQxmH35yIdf745u9CaHAImsiKuGYGJ3nohTacPuhzqDsqkiDiZC 14 | 61x8lHSDxc+dcLsDXPVwvT+k8U41Wd5HKy7gLAm0JWOtGPhyahwibRqz/eTsPyZe 15 | ZMvlOGM9y9eLzYIbomFNQHnPx2nHv38TaR7tYev8qfZxkHFwQETxoq8CgYEAz8mk 16 | gbWL8STVs0d7D9mswaH4aQUDU7AzejfEgtSxrHVNofkmVCl5KguCmoNcHv0jqvU/ 17 | cukETpYaMXEM/IRRlkSS0MQZGEHbCAr6gey3H3QZQSLbgGM0UxefePSgvXfkm0zG 18 | 6oLbviO4sefbY46Ys5xSefL2s5Xb/pq+5Suw7wsCgYEAvr5KFYABxSvV3XCXhLpG 19 | X4LFLLM6XcwwBQmEeSzBcALxQKz2ChD7nvajxM103N+Tzfd1NN7/Fjd/EhEk35ro 20 | 0ldiyytgqKw2Ny9AcTbCGCIQZoUqYOVzxFRmYPHB0Kv11U4wHWD6EET8vFGkPYmA 21 | f+C8WRKd+BgW/Gzx/zPrBgMCgYEAgpqivSjIikz1yZcPYdoXPSo6goA1JCAnaxWs 22 | ffOErfqZTkrVbacX0najo80XVR8VkTpPpEGUhHTSh+sgF4Rv57y4b2Iix9109+w2 23 | ov2P6MRHr2pif6NbWzMI+LUCZ7T5SygKC5Mu3aeESsaKXlxd3N9P8/jkWeLDAZhw 24 | jolU0BsCgYBrJx5sYfGuJDKncHjtL5TsD13hpqJAV+sgG2Qog8CQSUxka8KPxFvN 25 | tegW2puhyzhZ7+w3ZT4eacJgneTTl4E7xi4TJe1KZRWJ5Q82ide+NQWqmka2oWSL 26 | 5zYjidL35IksJifGBsGMduv6vEiFXNqBVTG2cEQiJ0JttXSrYOC2Wg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docker/elasticsearch/instances.yml: -------------------------------------------------------------------------------- 1 | instances: 2 | - name: es01 3 | dns: 4 | - es01 5 | - localhost 6 | ip: 7 | - 127.0.0.1 -------------------------------------------------------------------------------- /docker/search-api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Get the dotnet Build Environment: 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 3 | WORKDIR /source 4 | 5 | # COPY Project Files: 6 | COPY ../src/ElasticsearchFulltextExample.Api/*.csproj ./ElasticsearchFulltextExample.Api/ 7 | COPY ../src/ElasticsearchFulltextExample.Database/*.csproj ./ElasticsearchFulltextExample.Database/ 8 | COPY ../src/ElasticsearchFulltextExample.Shared/*.csproj ./ElasticsearchFulltextExample.Shared/ 9 | 10 | # And restore the NuGet Packages: 11 | RUN dotnet restore "ElasticsearchFulltextExample.Api/ElasticsearchFulltextExample.Api.csproj" 12 | 13 | # COPY 14 | COPY ../src/ElasticsearchFulltextExample.Api/. ./src/ElasticsearchFulltextExample.Api/ 15 | COPY ../src/ElasticsearchFulltextExample.Database/. ./src/ElasticsearchFulltextExample.Database/ 16 | COPY ../src/ElasticsearchFulltextExample.Shared/. ./src/ElasticsearchFulltextExample.Shared/ 17 | 18 | RUN dotnet publish ./src/ElasticsearchFulltextExample.Api/ElasticsearchFulltextExample.Api.csproj -c release -o /app 19 | 20 | # Build the final image 21 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 22 | 23 | # Copy Artifacts from Build: 24 | WORKDIR /app 25 | COPY --from=build /app ./ 26 | 27 | # Install Git 28 | RUN apt-get -y update 29 | RUN apt-get -y install git 30 | 31 | # Start the Kestrel Server: 32 | ENTRYPOINT ["dotnet", "ElasticsearchFulltextExample.Api.dll"] -------------------------------------------------------------------------------- /docker/search-web/Dockerfile: -------------------------------------------------------------------------------- 1 | # Get the dotnet Build Environment: 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 3 | WORKDIR /source 4 | 5 | # COPY Project Files: 6 | COPY ../src/ElasticsearchFulltextExample.Web.Client/*.csproj ./ElasticsearchFulltextExample.Web.Client/ 7 | COPY ../src/ElasticsearchFulltextExample.Web.Server/*.csproj ./ElasticsearchFulltextExample.Web.Server/ 8 | COPY ../src/ElasticsearchFulltextExample.Shared/*.csproj ./ElasticsearchFulltextExample.Shared/ 9 | 10 | # And restore the NuGet Packages: 11 | RUN dotnet restore "ElasticsearchFulltextExample.Web.Server/ElasticsearchFulltextExample.Web.Server.csproj" 12 | 13 | # COPY 14 | COPY ../src/ElasticsearchFulltextExample.Web.Client/. ./src/ElasticsearchFulltextExample.Web.Client/ 15 | COPY ../src/ElasticsearchFulltextExample.Web.Server/. ./src/ElasticsearchFulltextExample.Web.Server/ 16 | COPY ../src/ElasticsearchFulltextExample.Shared/. ./src/ElasticsearchFulltextExample.Shared/ 17 | 18 | RUN dotnet publish ./src/ElasticsearchFulltextExample.Web.Server/ElasticsearchFulltextExample.Web.Server.csproj -c Release -o /app 19 | 20 | # Build the final image 21 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 22 | 23 | # Copy Artifacts from Build: 24 | WORKDIR /app 25 | COPY --from=build /app ./ 26 | 27 | # Start the Kestrel Server: 28 | ENTRYPOINT ["dotnet", "ElasticsearchFulltextExample.Web.Server.dll"] -------------------------------------------------------------------------------- /sql/fts-data.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | 3 | BEGIN 4 | 5 | -- Initial Data 6 | INSERT INTO fts.user(user_id, email, preferred_name, last_edited_by) 7 | VALUES 8 | (1, 'philipp@bytefish.de', 'Data Conversion User', 1) 9 | ON CONFLICT DO NOTHING; 10 | 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | -------------------------------------------------------------------------------- /sql/fts-replication.sql: -------------------------------------------------------------------------------- 1 | DO $$ 2 | 3 | BEGIN 4 | 5 | IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_publication WHERE pubname = 'outbox_pub') 6 | THEN 7 | CREATE PUBLICATION outbox_pub FOR TABLE 8 | fts.outbox_event; 9 | END IF; 10 | 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | 14 | SELECT 'outbox_slot_init' FROM pg_create_logical_replication_slot('outbox_slot', 'pgoutput'); 15 | -------------------------------------------------------------------------------- /sql/fts-tests.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE PROCEDURE fts.cleanup_tests() 2 | AS $cleanup_tests_func$ 3 | BEGIN 4 | 5 | -- Delete all non-fixed data 6 | DELETE FROM fts.document_keyword; 7 | DELETE FROM fts.document_suggestion; 8 | DELETE FROM fts.keyword; 9 | DELETE FROM fts.suggestion; 10 | DELETE FROM fts.document; 11 | DELETE FROM fts.outbox_event; 12 | 13 | DELETE FROM fts.user WHERE user_id != 1; 14 | 15 | -- Delete historic data 16 | DELETE FROM fts.document_keyword_history; 17 | DELETE FROM fts.document_suggestion_history; 18 | DELETE FROM fts.keyword_history; 19 | DELETE FROM fts.suggestion_history; 20 | DELETE FROM fts.document_history; 21 | DELETE FROM fts.outbox_event_history; 22 | 23 | DELETE FROM fts.user_history WHERE user_id != 1; 24 | 25 | 26 | END; $cleanup_tests_func$ 27 | LANGUAGE plpgsql; -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Configuration/ApplicationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace ElasticsearchFulltextExample.Api.Configuration 5 | { 6 | public class ApplicationOptions 7 | { 8 | public required string BaseUri { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Configuration/ElasticsearchOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace ElasticsearchFulltextExample.Api.Configuration 5 | { 6 | public class ElasticsearchOptions 7 | { 8 | public required string Uri { get; set; } 9 | 10 | public required string IndexName { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Configuration/IndexerOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace ElasticsearchFulltextExample.Api.Configuration 5 | { 6 | public class IndexerOptions 7 | { 8 | public int IndexDelay { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Configuration/PostgresOutboxEventProcessorOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Configuration 4 | { 5 | public class PostgresOutboxEventProcessorOptions 6 | { 7 | /// 8 | /// Gets or sets the ConnectionString for the Replication Stream. 9 | /// 10 | public required string ConnectionString { get; set; } 11 | 12 | /// 13 | /// Gets or sets the PublicationName the Service is listening to. 14 | /// 15 | public required string PublicationName { get; set; } 16 | 17 | /// 18 | /// Gets or sets the ReplicationSlot the Service is listening to. 19 | /// 20 | public required string ReplicationSlotName { get; set; } 21 | 22 | /// 23 | /// Gets or sets the Table the Outbox Events are written to. 24 | /// 25 | public required string OutboxEventTableName { get; set; } 26 | 27 | /// 28 | /// Gets or sets the Schema the Outbox Events are written to. 29 | /// 30 | public required string OutboxEventSchemaName { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Constants/Policies.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Constants 4 | { 5 | /// 6 | /// Authorization Policies. 7 | /// 8 | public class Policies 9 | { 10 | /// 11 | /// Requires the User Role to be set. 12 | /// 13 | public const string RequireUserRole = "RequireUserRole"; 14 | 15 | /// 16 | /// Required the Admin Role to be set. 17 | /// 18 | public const string RequireAdminRole = "RequireAdminRole"; 19 | 20 | /// 21 | /// Per-User Rate Limiting Policy. 22 | /// 23 | public const string PerUserRatelimit = "PerUserRatelimit"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Constants/Roles.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Constants 4 | { 5 | /// 6 | /// Roles. 7 | /// 8 | public static class Roles 9 | { 10 | /// 11 | /// User. 12 | /// 13 | public const string User = "User"; 14 | 15 | /// 16 | /// Administrator. 17 | /// 18 | public const string Administrator = "Administrator"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Constants/Users.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Constants 4 | { 5 | /// 6 | /// Roles. 7 | /// 8 | public static class Users 9 | { 10 | /// 11 | /// DataConversionUser. 12 | /// 13 | public const int DataConversionUserId = 1; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/ElasticsearchFulltextExample.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Hosting/ElasticsearchInitializerBackgroundService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using ElasticsearchFulltextExample.Api.Infrastructure.Elasticsearch; 5 | using ElasticsearchFulltextExample.Shared.Infrastructure; 6 | 7 | namespace ElasticsearchFulltextExample.Api.Hosting 8 | { 9 | public class ElasticsearchInitializerBackgroundService : BackgroundService 10 | { 11 | private readonly ILogger _logger; 12 | 13 | private readonly ElasticsearchSearchClient _elasticsearchClient; 14 | 15 | public ElasticsearchInitializerBackgroundService(ILogger logger, ElasticsearchSearchClient elasticsearchClient) 16 | { 17 | _logger = logger; 18 | _elasticsearchClient = elasticsearchClient; 19 | } 20 | 21 | protected override async Task ExecuteAsync(CancellationToken cancellationToken) 22 | { 23 | var healthTimeout = TimeSpan.FromSeconds(50); 24 | 25 | if (_logger.IsDebugEnabled()) 26 | { 27 | _logger.LogDebug($"Waiting for at least 1 Node, with a Timeout of '{healthTimeout.TotalSeconds}' seconds."); 28 | } 29 | 30 | await _elasticsearchClient.WaitForClusterAsync(healthTimeout, cancellationToken); 31 | 32 | var indexExistsResponse = await _elasticsearchClient.IndexExistsAsync(cancellationToken); 33 | 34 | if (!indexExistsResponse.Exists) 35 | { 36 | await _elasticsearchClient.CreateIndexAsync(cancellationToken); 37 | await _elasticsearchClient.CreatePipelineAsync(cancellationToken); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Hosting/PostgresOutboxEventProcessor.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Configuration; 4 | using ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Consumer; 5 | using ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Postgres; 6 | using ElasticsearchFulltextExample.Shared.Infrastructure; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace ElasticsearchFulltextExample.Api.Hosting 10 | { 11 | /// 12 | /// Processes Outbox Events. 13 | /// 14 | public class PostgresOutboxEventProcessor : BackgroundService 15 | { 16 | private readonly ILogger _logger; 17 | 18 | private readonly PostgresOutboxEventProcessorOptions _options; 19 | private readonly OutboxEventConsumer _outboxEventConsumer; 20 | 21 | public PostgresOutboxEventProcessor(ILogger logger, IOptions options, OutboxEventConsumer outboxEventConsumer) 22 | { 23 | _logger = logger; 24 | _options = options.Value; 25 | _outboxEventConsumer = outboxEventConsumer; 26 | } 27 | 28 | protected override async Task ExecuteAsync(CancellationToken cancellationToken) 29 | { 30 | _logger.TraceMethodEntry(); 31 | 32 | var outboxSubscriberOptions = new PostgresOutboxSubscriberOptions 33 | { 34 | ConnectionString = _options.ConnectionString, 35 | PublicationName = _options.PublicationName, 36 | ReplicationSlotName = _options.ReplicationSlotName, 37 | OutboxEventSchemaName = _options.OutboxEventSchemaName, 38 | OutboxEventTableName = _options.OutboxEventTableName 39 | }; 40 | 41 | var outboxEventStream = new PostgresOutboxSubscriber(_logger, Options.Create(outboxSubscriberOptions)); 42 | 43 | while (!cancellationToken.IsCancellationRequested) 44 | { 45 | try 46 | { 47 | await foreach (var outboxEvent in outboxEventStream.StartOutboxEventStreamAsync(cancellationToken)) 48 | { 49 | _logger.LogInformation("Processing OutboxEvent (Id = {OutboxEventId})", outboxEvent.Id); 50 | 51 | try 52 | { 53 | await _outboxEventConsumer 54 | .ConsumeOutboxEventAsync(outboxEvent, cancellationToken) 55 | .ConfigureAwait(false); 56 | } 57 | catch (Exception e) 58 | { 59 | _logger.LogError(e, "Failed to handle the OutboxEvent due to an Exception (ID = {OutboxEventId})", outboxEvent.Id); 60 | } 61 | } 62 | } 63 | catch (Exception e) 64 | { 65 | _logger.LogError(e, "Logical Replication failed with an Error. Restarting the Stream."); 66 | 67 | // Probably add some better Retry options ... 68 | await Task 69 | .Delay(30_000) // Reconnect every 30 Seconds 70 | .ConfigureAwait(false); 71 | } 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Authentication/ClaimsPrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Security.Claims; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Authentication 6 | { 7 | public static class ClaimsPrincipalExtensions 8 | { 9 | public static int GetUserId(this ClaimsPrincipal user) 10 | { 11 | var userId = user.FindFirstValue(ClaimTypes.Sid); 12 | 13 | if (userId == null) 14 | { 15 | throw new InvalidOperationException("No UserID found for ClaimsPrincipal"); 16 | } 17 | 18 | if (!int.TryParse(userId, out var result)) 19 | { 20 | throw new InvalidOperationException("UserID could not be converted to an Int32"); 21 | } 22 | 23 | return result; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Authentication/CurrentUser.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Database.Model; 4 | using System.Security.Claims; 5 | 6 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Authentication 7 | { 8 | /// 9 | /// A Scoped Service to provide the current user information. 10 | /// 11 | public class CurrentUser 12 | { 13 | /// 14 | /// Gets or sets the User. 15 | /// 16 | public User? User { get; set; } 17 | 18 | /// 19 | /// Gets or sets the . 20 | /// 21 | public ClaimsPrincipal Principal { get; set; } = default!; 22 | 23 | /// 24 | /// Gets the UserID for the current . 25 | /// 26 | public int UserId => Principal.GetUserId(); 27 | 28 | /// 29 | /// Checks if the User is Administrator 30 | /// 31 | public bool IsInRole(string role) 32 | { 33 | return Principal.IsInRole(role); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Authentication/CurrentUserClaimsTransformation.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Services; 4 | using Microsoft.AspNetCore.Authentication; 5 | using System.Security.Claims; 6 | 7 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Authentication 8 | { 9 | public class CurrentUserClaimsTransformation : IClaimsTransformation 10 | { 11 | private readonly CurrentUser _currentUser; 12 | private readonly UserService _userService; 13 | 14 | public CurrentUserClaimsTransformation(CurrentUser currentUser, UserService userService) 15 | { 16 | _currentUser = currentUser; 17 | _userService = userService; 18 | } 19 | 20 | public async Task TransformAsync(ClaimsPrincipal principal) 21 | { 22 | _currentUser.Principal = principal; 23 | 24 | if (principal.FindFirstValue(ClaimTypes.NameIdentifier) is { Length: > 0 } name) 25 | { 26 | _currentUser.User = await _userService.GetUserByEmailAsync(name, default); // Where do we get the CancellationToken from? 27 | } 28 | 29 | return principal; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Elasticsearch/ElasticsearchConstants.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Elasticsearch 4 | { 5 | /// 6 | /// Constants used by the Frontend and Backend. 7 | /// 8 | public static class ElasticsearchConstants 9 | { 10 | /// 11 | /// A tag used to find the highlightning start position. 12 | /// 13 | public static readonly string HighlightStartTag = "elasticsearch→"; 14 | 15 | /// 16 | /// A tag used to find the highlightning end position. 17 | /// 18 | public static readonly string HighlightEndTag = "←elasticsearch"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Elasticsearch/ElasticsearchSearchClientOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Elasticsearch 4 | { 5 | /// 6 | /// Elasticsearch options. 7 | /// 8 | public class ElasticsearchSearchClientOptions 9 | { 10 | /// 11 | /// Endpoint of the Elasticsearch Node. 12 | /// 13 | public required string Uri { get; set; } 14 | 15 | /// 16 | /// Index to use for Code Search. 17 | /// 18 | public required string IndexName { get; set; } 19 | 20 | /// 21 | /// Elasticsearch Username. 22 | /// 23 | public required string Username { get; set; } 24 | 25 | /// 26 | /// Elasticsearch Password. 27 | /// 28 | public required string Password { get; set; } 29 | 30 | /// 31 | /// Certificate Fingerprint for trusting the Certificate. 32 | /// 33 | public required string CertificateFingerprint { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Elasticsearch/Models/ElasticsearchDocument.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Shared.Constants; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Elasticsearch.Models 7 | { 8 | public class ElasticsearchDocument 9 | { 10 | /// 11 | /// A unique document id. 12 | /// 13 | [JsonPropertyName(ElasticConstants.DocumentNames.Id)] 14 | public required string Id { get; set; } 15 | 16 | /// 17 | /// The Title of the Document for Suggestion. 18 | /// 19 | [JsonPropertyName(ElasticConstants.DocumentNames.Title)] 20 | public required string Title { get; set; } 21 | 22 | /// 23 | /// The Original Filename of the uploaded document. 24 | /// 25 | [JsonPropertyName(ElasticConstants.DocumentNames.Filename)] 26 | public required string Filename { get; set; } 27 | 28 | /// 29 | /// The Data of the Document. 30 | /// 31 | [JsonPropertyName(ElasticConstants.DocumentNames.Data)] 32 | public byte[]? Data { get; set; } 33 | 34 | /// 35 | /// Keywords to filter for. 36 | /// 37 | [JsonPropertyName(ElasticConstants.DocumentNames.Keywords)] 38 | public required string[] Keywords { get; set; } 39 | 40 | /// 41 | /// Suggestions for the Autocomplete Field. 42 | /// 43 | [JsonPropertyName(ElasticConstants.DocumentNames.Suggestions)] 44 | public required string[] Suggestions { get; set; } 45 | 46 | /// 47 | /// The Date the document was indexed on. 48 | /// 49 | [JsonPropertyName(ElasticConstants.DocumentNames.IndexedOn)] 50 | public DateTime? IndexedOn { get; set; } 51 | 52 | /// 53 | /// The Attachment generated by Elasticsearch. 54 | /// 55 | [JsonPropertyName(ElasticConstants.DocumentNames.Attachment)] 56 | public ElasticsearchAttachment? Attachment { get; set; } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Errors/ApplicationErrorResult.cs: -------------------------------------------------------------------------------- 1 | using ElasticsearchFulltextExample.Api.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Errors 5 | { 6 | /// 7 | /// Represents a result that when executed will produce an . 8 | /// 9 | /// This result creates an response. 10 | public class ApplicationErrorResult : ActionResult 11 | { 12 | /// 13 | /// OData error. 14 | /// 15 | public required ApplicationError Error { get; set; } 16 | 17 | /// 18 | /// Http Status Code. 19 | /// 20 | public required int HttpStatusCode { get; set; } 21 | 22 | /// 23 | public async override Task ExecuteResultAsync(ActionContext context) 24 | { 25 | ObjectResult objectResult = new ObjectResult(Error) 26 | { 27 | StatusCode = HttpStatusCode 28 | }; 29 | 30 | await objectResult 31 | .ExecuteResultAsync(context) 32 | .ConfigureAwait(false); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Errors/ExceptionToApplicationErrorMapper.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Infrastructure.Exceptions; 4 | using ElasticsearchFulltextExample.Api.Models; 5 | using ElasticsearchFulltextExample.Shared.Infrastructure; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Errors 9 | { 10 | /// 11 | /// Handles errors returned by the application. 12 | /// 13 | public class ExceptionToApplicationErrorMapper 14 | { 15 | private readonly ILogger _logger; 16 | 17 | private readonly ExceptionToApplicationErrorMapperOptions _options; 18 | private readonly Dictionary _translators; 19 | 20 | public ExceptionToApplicationErrorMapper(ILogger logger, IOptions options, IEnumerable translators) 21 | { 22 | _logger = logger; 23 | _options = options.Value; 24 | _translators = translators.ToDictionary(x => x.ExceptionType, x => x); 25 | } 26 | 27 | public ApplicationErrorResult CreateApplicationErrorResult(HttpContext httpContext, Exception exception) 28 | { 29 | _logger.TraceMethodEntry(); 30 | 31 | _logger.LogError(exception, "Call to '{RequestPath}' failed due to an Exception", httpContext.Request.Path); 32 | 33 | // Get the best matching translator for the exception ... 34 | var translator = GetTranslator(exception); 35 | 36 | // ... translate it to the Result ... 37 | var error = translator.GetApplicationErrorResult(exception, _options.IncludeExceptionDetails); 38 | 39 | // ... add error metadata, such as a Trace ID, ... 40 | AddMetadata(httpContext, error); 41 | 42 | // ... and return it. 43 | return error; 44 | } 45 | 46 | private void AddMetadata(HttpContext httpContext, ApplicationErrorResult result) 47 | { 48 | if (result.Error.InnerError == null) 49 | { 50 | result.Error.InnerError = new ApplicationInnerError(); 51 | } 52 | 53 | result.Error.InnerError.AdditionalProperties["trace-id"] = httpContext.TraceIdentifier; 54 | } 55 | 56 | private IExceptionTranslator GetTranslator(Exception e) 57 | { 58 | if (e is ApplicationErrorException) 59 | { 60 | if (_translators.TryGetValue(e.GetType(), out var translator)) 61 | { 62 | return translator; 63 | } 64 | 65 | return _translators[typeof(ApplicationErrorException)]; 66 | } 67 | 68 | return _translators[typeof(Exception)]; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Errors/ExceptionToApplicationErrorMapperOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Errors 4 | { 5 | /// 6 | /// Options for the . 7 | /// 8 | public class ExceptionToApplicationErrorMapperOptions 9 | { 10 | /// 11 | /// Gets or sets the option to include the Exception Details in the response. 12 | /// 13 | public bool IncludeExceptionDetails { get; set; } = false; 14 | } 15 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Errors/IExceptionTranslator.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Errors 6 | { 7 | /// 8 | /// A Translator to convert from an to an . 9 | /// 10 | public interface IExceptionTranslator 11 | { 12 | /// 13 | /// Translates a given into an . 14 | /// 15 | /// Exception to translate 16 | /// A flag, if exception details should be included 17 | /// The for the 18 | ApplicationErrorResult GetApplicationErrorResult(Exception exception, bool includeExceptionDetails); 19 | 20 | /// 21 | /// Gets or sets the Exception Type. 22 | /// 23 | Type ExceptionType { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Errors/Translators/ApplicationErrorExceptionTranslator.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Infrastructure.Exceptions; 4 | using ElasticsearchFulltextExample.Api.Models; 5 | using ElasticsearchFulltextExample.Shared.Infrastructure; 6 | 7 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Errors.Translators 8 | { 9 | public class ApplicationErrorExceptionTranslator : IExceptionTranslator 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public ApplicationErrorExceptionTranslator(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | /// 19 | public ApplicationErrorResult GetApplicationErrorResult(Exception exception, bool includeExceptionDetails) 20 | { 21 | _logger.TraceMethodEntry(); 22 | 23 | var applicationErrorException = (ApplicationErrorException)exception; 24 | 25 | return InternalGetApplicationErrorResult(applicationErrorException, includeExceptionDetails); 26 | } 27 | 28 | private ApplicationErrorResult InternalGetApplicationErrorResult(ApplicationErrorException exception, bool includeExceptionDetails) 29 | { 30 | var error = new ApplicationError 31 | { 32 | Code = exception.ErrorCode, 33 | Message = exception.ErrorMessage, 34 | }; 35 | 36 | error.InnerError = new ApplicationInnerError(); 37 | 38 | // Create the Inner Error 39 | if (includeExceptionDetails) 40 | { 41 | error.InnerError.Message = exception.Message; 42 | error.InnerError.StackTrace = exception.StackTrace; 43 | error.InnerError.Target = exception.GetType().Name; 44 | } 45 | 46 | return new ApplicationErrorResult 47 | { 48 | Error = error, 49 | HttpStatusCode = exception.HttpStatusCode, 50 | }; 51 | } 52 | 53 | /// 54 | public Type ExceptionType => typeof(ApplicationErrorException); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Errors/Translators/DefaultExceptionTranslator.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Models; 4 | using ElasticsearchFulltextExample.Shared.Infrastructure; 5 | 6 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Errors.Translators 7 | { 8 | public class DefaultExceptionTranslator : IExceptionTranslator 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public DefaultExceptionTranslator(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public Type ExceptionType => typeof(Exception); 18 | 19 | public ApplicationErrorResult GetApplicationErrorResult(Exception exception, bool includeExceptionDetails) 20 | { 21 | _logger.TraceMethodEntry(); 22 | 23 | var error = new ApplicationError 24 | { 25 | Code = ErrorCodes.InternalServerError, 26 | Message = "An Internal Server Error occured" 27 | }; 28 | 29 | // Create the Inner Error 30 | error.InnerError = new ApplicationInnerError(); 31 | 32 | if (includeExceptionDetails) 33 | { 34 | error.InnerError.Message = exception.Message; 35 | error.InnerError.StackTrace = exception.StackTrace; 36 | error.InnerError.Target = exception.GetType().Name; 37 | } 38 | 39 | return new ApplicationErrorResult 40 | { 41 | Error = error, 42 | HttpStatusCode = StatusCodes.Status500InternalServerError, 43 | }; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/ApplicationErrorException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 6 | { 7 | /// 8 | /// Base Exception for the Application. 9 | /// 10 | public abstract class ApplicationErrorException : Exception 11 | { 12 | /// 13 | /// Gets the Error Code. 14 | /// 15 | public abstract string ErrorCode { get; } 16 | 17 | /// 18 | /// Gets the Error Message. 19 | /// 20 | public abstract string ErrorMessage { get; } 21 | 22 | /// 23 | /// Gets the HttpStatusCode. 24 | /// 25 | public abstract int HttpStatusCode { get; } 26 | 27 | protected ApplicationErrorException(string message, Exception? innerException) 28 | : base(message, innerException) 29 | { 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/AuthenticationFailedException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Infrastructure.Errors; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 6 | { 7 | public class AuthenticationFailedException : ApplicationErrorException 8 | { 9 | /// 10 | public override string ErrorCode => ErrorCodes.AuthenticationFailed; 11 | 12 | /// 13 | public override string ErrorMessage => $"AuthenticationFailed"; 14 | 15 | /// 16 | public override int HttpStatusCode => StatusCodes.Status401Unauthorized; 17 | 18 | /// 19 | /// Creates a new . 20 | /// 21 | /// Error Message 22 | /// Reference to the Inner Exception 23 | public AuthenticationFailedException(string message = "AuthenticationFailed", Exception? innerException = null) 24 | : base(message, innerException) 25 | { 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/AuthorizationFailedException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Infrastructure.Errors; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 6 | { 7 | public class AuthorizationFailedException : ApplicationErrorException 8 | { 9 | /// 10 | public override string ErrorCode => ErrorCodes.AuthorizationFailed; 11 | 12 | /// 13 | public override string ErrorMessage => $"AuthorizationFailed"; 14 | 15 | /// 16 | public override int HttpStatusCode => StatusCodes.Status403Forbidden; 17 | 18 | /// 19 | /// Creates a new . 20 | /// 21 | /// Error Message 22 | /// Reference to the Inner Exception 23 | public AuthorizationFailedException(string message, Exception? innerException = null) 24 | : base(message, innerException) 25 | { 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/CannotDeleteOwnUserException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Infrastructure.Errors; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 6 | { 7 | public class CannotDeleteOwnUserException : ApplicationErrorException 8 | { 9 | /// 10 | public override string ErrorCode => ErrorCodes.CannotDeleteOwnUser; 11 | 12 | /// 13 | public override string ErrorMessage => $"CannotDeleteOwnUserException (UserId = {UserId})"; 14 | 15 | /// 16 | public override int HttpStatusCode => StatusCodes.Status428PreconditionRequired; 17 | 18 | /// 19 | /// Gets or sets the UserId. 20 | /// 21 | public required int UserId { get; set; } 22 | 23 | /// 24 | /// Creates a new . 25 | /// 26 | /// Error Message 27 | /// Reference to the Inner Exception 28 | public CannotDeleteOwnUserException(string message = "CannotDeleteOwnUser", Exception? innerException = null) 29 | : base(message, innerException) 30 | { 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/EntityConcurrencyException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | 4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 5 | 6 | using ElasticsearchFulltextExample.Api.Infrastructure.Errors; 7 | 8 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 9 | { 10 | public class EntityConcurrencyException : ApplicationErrorException 11 | { 12 | /// 13 | public override string ErrorCode => ErrorCodes.EntityConcurrencyFailure; 14 | 15 | /// 16 | public override string ErrorMessage => $"EntityConcurrencyFailure (Entity = {EntityName}, EntityID = {EntityId})"; 17 | 18 | /// 19 | public override int HttpStatusCode => StatusCodes.Status409Conflict; 20 | 21 | /// 22 | /// Gets or sets the Entity Name. 23 | /// 24 | public required string EntityName { get; set; } 25 | 26 | /// 27 | /// Gets or sets the EntityId. 28 | /// 29 | public required int EntityId { get; set; } 30 | 31 | /// 32 | /// Creates a new . 33 | /// 34 | /// Error Message 35 | /// Reference to the Inner Exception 36 | public EntityConcurrencyException(string message = "EntityConcurrencyFailure", Exception? innerException = null) 37 | : base(message, innerException) 38 | { 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | 4 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 5 | 6 | using ElasticsearchFulltextExample.Api.Infrastructure.Errors; 7 | 8 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 9 | { 10 | public class EntityNotFoundException : ApplicationErrorException 11 | { 12 | /// 13 | public override string ErrorCode => ErrorCodes.EntityNotFound; 14 | 15 | /// 16 | public override string ErrorMessage => $"EntityNotFound (Entity = {EntityName}, EntityID = {EntityId})"; 17 | 18 | /// 19 | public override int HttpStatusCode => StatusCodes.Status404NotFound; 20 | 21 | /// 22 | /// Gets or sets the Entity Name. 23 | /// 24 | public required string EntityName { get; set; } 25 | 26 | /// 27 | /// Gets or sets the EntityId. 28 | /// 29 | public required int EntityId { get; set; } 30 | 31 | /// 32 | /// Creates a new . 33 | /// 34 | /// Error Message 35 | /// Reference to the Inner Exception 36 | public EntityNotFoundException(string message = "EntityNotFound", Exception? innerException = null) 37 | : base(message, innerException) 38 | { 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Exceptions/InvalidModelStateException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Api.Infrastructure.Errors; 4 | using Microsoft.AspNetCore.Mvc.ModelBinding; 5 | 6 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Exceptions 7 | { 8 | public class InvalidModelStateException : ApplicationErrorException 9 | { 10 | /// 11 | public override string ErrorCode => ErrorCodes.ValidationFailed; 12 | 13 | /// 14 | public override string ErrorMessage => $"ValidationFailure"; 15 | 16 | /// 17 | public override int HttpStatusCode => StatusCodes.Status400BadRequest; 18 | 19 | /// 20 | /// Gets or sets the ModelStateDictionary. 21 | /// 22 | public required ModelStateDictionary ModelStateDictionary { get; set; } 23 | 24 | /// 25 | /// Creates a new . 26 | /// 27 | /// Error Message 28 | /// Reference to the Inner Exception 29 | public InvalidModelStateException(string message = "InvalidModelState", Exception? innerException = null) 30 | : base(message, innerException) 31 | { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Mvc/EnumRouteConstraint.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Mvc 4 | { 5 | public class EnumRouteConstraint : IRouteConstraint 6 | where TEnum : struct 7 | { 8 | public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) 9 | { 10 | var matchingValue = values[routeKey]?.ToString(); 11 | 12 | return Enum.TryParse(matchingValue, true, out TEnum _); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Outbox/Consumer/IOutboxEventConsumer.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Database.Model; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Consumer 6 | { 7 | /// 8 | /// An interface for consuming a single . 9 | /// 10 | public interface IOutboxEventConsumer 11 | { 12 | /// 13 | /// Consumes a given . 14 | /// 15 | /// Outbox Event generated by an Event Producer 16 | /// Cancellation Token to cancel downstream asynchronous calls 17 | /// Awaitable Task 18 | Task ConsumeOutboxEventAsync(OutboxEvent outboxEvent, CancellationToken cancellationToken); 19 | } 20 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Outbox/Messages/DocumentCreatedMessage.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Messages 6 | { 7 | /// 8 | /// A Document has been created. 9 | /// 10 | public class DocumentCreatedMessage 11 | { 12 | /// 13 | /// Gets or sets the Document ID. 14 | /// 15 | [JsonPropertyName("documentId")] 16 | public int DocumentId { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Outbox/Messages/DocumentDeletedMessage.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Messages 6 | { 7 | /// 8 | /// A Document has been deleted. 9 | /// 10 | public class DocumentDeletedMessage 11 | { 12 | /// 13 | /// Gets or sets the Document ID. 14 | /// 15 | [JsonPropertyName("documentId")] 16 | public int DocumentId { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Outbox/Messages/DocumentUpdatedMessage.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Messages 6 | { 7 | /// 8 | /// A Document has been updated. 9 | /// 10 | public class DocumentUpdatedMessage 11 | { 12 | /// 13 | /// Gets or sets the Document ID. 14 | /// 15 | [JsonPropertyName("documentId")] 16 | public int DocumentId { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Outbox/OutboxEventUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Database.Model; 4 | using System.Text.Json; 5 | 6 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Outbox 7 | { 8 | /// 9 | /// Static Methods to simplify working with a . 10 | /// 11 | public static class OutboxEventUtils 12 | { 13 | /// 14 | /// Creates a new from a given message Payload. 15 | /// 16 | /// Type of the Message 17 | /// Message Payload 18 | /// User that created the Outbox Event 19 | /// An that could be used 20 | public static OutboxEvent Create(TMessageType message, int lastEditedBy) 21 | { 22 | var outboxEvent = new OutboxEvent 23 | { 24 | EventType = typeof(TMessageType).FullName!, 25 | Payload = JsonSerializer.SerializeToDocument(message), 26 | LastEditedBy = lastEditedBy 27 | }; 28 | 29 | return outboxEvent; 30 | } 31 | 32 | /// 33 | /// Tries to get the deserialize the JSON Payload to the Type given in the 34 | /// . This returns an , so you 35 | /// should do pattern matching on the consumer-side. 36 | /// 37 | /// Outbox Event with typed Payload 38 | /// The Payload deserialized to the Event Type 39 | /// , if the payload can be deserialized; else 40 | public static bool TryGetOutboxEventPayload(this OutboxEvent outboxEvent, out object? result) 41 | { 42 | result = null; 43 | 44 | // Maybe throw here? We should probably log it at least... 45 | var type = Type.GetType(outboxEvent.EventType, throwOnError: false); 46 | 47 | if (type == null) 48 | { 49 | return false; 50 | } 51 | 52 | result = outboxEvent.Payload.Deserialize(type); 53 | 54 | return true; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Infrastructure/Outbox/Postgres/PostgresOutboxSubscriberOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Infrastructure.Outbox.Postgres 4 | { 5 | /// 6 | /// Options to configure the . 7 | /// 8 | public class PostgresOutboxSubscriberOptions 9 | { 10 | /// 11 | /// Gets or sets the ConnectionString for the Replication Stream. 12 | /// 13 | public required string ConnectionString { get; set; } 14 | 15 | /// 16 | /// Gets or sets the PublicationName the Service is listening to. 17 | /// 18 | public required string PublicationName { get; set; } 19 | 20 | /// 21 | /// Gets or sets the ReplicationSlot the Service is listening to. 22 | /// 23 | public required string ReplicationSlotName { get; set; } 24 | 25 | /// 26 | /// Gets or sets the Table the Outbox Events are written to. 27 | /// 28 | public required string OutboxEventTableName { get; set; } 29 | 30 | /// 31 | /// Gets or sets the Schema the Outbox Events are written to. 32 | /// 33 | public required string OutboxEventSchemaName { get; set; } 34 | } 35 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/ApplicationError.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Api.Models 4 | { 5 | /// 6 | /// Represents an error payload. 7 | /// 8 | public class ApplicationError 9 | { 10 | /// 11 | /// The value for the code name/value pair is a non-empty language-independent string. Its value is a service-defined 12 | /// error code.This code serves as a sub-status for the HTTP error code specified in the response.It cannot be null. 13 | /// 14 | public required string Code { get; set; } 15 | 16 | /// 17 | /// The value for the message name/value pair is a non-empty, language-dependent, human-readable string describing the 18 | /// error.The Content-Language header MUST contain the language code from [RFC5646] corresponding to the language in 19 | /// which the value for message is written.It cannot be null. 20 | /// 21 | public required string Message { get; set; } 22 | 23 | /// 24 | /// The value for the target name/value pair is a potentially empty string indicating the target of the error (for example, 25 | /// the name of the property in error). It can be null. 26 | /// 27 | public string? Target { get; set; } 28 | 29 | /// 30 | /// The value for the details name/value pair MUST be an array of JSON objects that MUST contain name/value pairs for code and 31 | /// message, and MAY contain a name/value pair for target. 32 | /// 33 | public List Details { get; set; } = new(); 34 | 35 | /// 36 | /// The value for the innererror name/value pair MUST be an object. The contents of this object are service-defined. Usually 37 | /// this object contains information that will help debug the service. 38 | /// 39 | public ApplicationInnerError? InnerError { get; set; } 40 | 41 | /// 42 | /// Additional Properties. 43 | /// 44 | [JsonExtensionData] 45 | public Dictionary AdditionalProperties { get; set; } = new(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/ApplicationErrorDetail.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Api.Models 4 | { 5 | /// 6 | /// Represents an error detail. 7 | /// 8 | public class ApplicationErrorDetail 9 | { 10 | 11 | /// 12 | /// The value for the code name/value pair is a non-empty language-independent string. Its value is a 13 | /// service-defined error code.This code serves as a sub-status for the HTTP error code specified in 14 | /// the response.It cannot be null. 15 | /// 16 | public required string ErrorCode { get; set; } 17 | 18 | /// 19 | /// The value for the message name/value pair is a non-empty, language-dependent, human-readable string describing 20 | /// the error.The Content-Language header MUST contain the language code from [RFC5646] corresponding to the language 21 | /// in which the value for message is written.It cannot be null. 22 | /// 23 | public required string Message { get; set; } 24 | 25 | /// 26 | /// The value for the target name/value pair is a potentially empty string indicating the target of the error (for 27 | /// example, the name of the property in error). It can be null. 28 | /// 29 | public required string Target { get; set; } 30 | 31 | /// 32 | /// Additional Properties. 33 | /// 34 | [JsonExtensionData] 35 | public Dictionary AdditionalProperties { get; set; } = new(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/ApplicationInnerError.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Api.Models 4 | { 5 | /// 6 | /// Class representing implementation specific debugging information to help determine the cause of the error. 7 | /// 8 | public class ApplicationInnerError 9 | { 10 | /// 11 | /// The value for the message name/value pair is a non-empty, language-dependent, human-readable string describing 12 | /// the error. The Content-Language header MUST contain the language code from[RFC5646] corresponding to the 13 | /// language in which the value for message is written.It cannot be null. 14 | /// 15 | public string? Message { get; set; } 16 | 17 | /// 18 | /// The value for the target name/value pair is a potentially empty string indicating the target of the 19 | /// error (for example, the name of the property in error). It can be null. 20 | /// 21 | public string? Target { get; set; } 22 | 23 | /// 24 | /// Additional Debugging information, such as a Stack Trace. 25 | /// 26 | public string? StackTrace { get; set; } 27 | 28 | /// 29 | /// A nested inner error. 30 | /// 31 | public ApplicationInnerError? InnerError { get; set; } 32 | 33 | /// 34 | /// Additional Properties. 35 | /// 36 | [JsonExtensionData] 37 | public Dictionary AdditionalProperties { get; set; } = new(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/HighlightedContent.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Api.Models 4 | { 5 | /// 6 | /// Holds the line number and line content for a match, and it 7 | /// has the information if the content needs highlighting. 8 | /// 9 | public class HighlightedContent 10 | { 11 | /// 12 | /// Gets or sets the line number. 13 | /// 14 | public int LineNo { get; set; } 15 | 16 | /// 17 | /// Gets or sets the line content. 18 | /// 19 | public string Content { get; set; } = string.Empty; 20 | 21 | /// 22 | /// Gets or sets the flag, if this line needs to be highlighted. 23 | /// 24 | public bool IsHighlight { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/SearchResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace ElasticsearchFulltextExample.Api.Models 5 | { 6 | public class SearchResult 7 | { 8 | public required string Identifier { get; set; } 9 | 10 | public required string Title { get; set; } 11 | 12 | public required string Filename { get; set; } 13 | 14 | public List Matches { get; set; } = new(); 15 | 16 | public List Keywords { get; set; } = new(); 17 | 18 | public string? Url { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/SearchResults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace ElasticsearchFulltextExample.Api.Models 5 | { 6 | public class SearchResults 7 | { 8 | public required string Query { get; set; } 9 | 10 | public required int From { get; set; } 11 | 12 | public required int Size { get; set; } 13 | 14 | public required long Total { get; set; } 15 | 16 | public required long TookInMilliseconds { get; set; } 17 | 18 | public List Results { get; set; } = new(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/SearchSuggestion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Api.Models 4 | { 5 | public class SearchSuggestion 6 | { 7 | public required string Text { get; set; } 8 | 9 | public required string Highlight { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Models/SearchSuggestionsDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Api.Models 4 | { 5 | public class SearchSuggestions 6 | { 7 | public required string Query { get; set; } 8 | 9 | 10 | [JsonPropertyName("results")] 11 | public List Results { get; set; } = new(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:9000", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | "https": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "applicationUrl": "https://localhost:5000" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationDatabase": "Server=localhost;Port=5432;Database=postgres;User Id=postgres;Password=password;" 4 | }, 5 | "Elasticsearch": { 6 | "Uri": "https://localhost:9200", 7 | "IndexName": "documents", 8 | "Username": "elastic", 9 | "Password": "secret", 10 | "CertificateFingerprint": "31a63ffca5275df7ea7d6fc7e92b42cfa774a0feed7d7fa8488c5e46ea9ade3f" 11 | }, 12 | "AllowedHosts": "localhost", 13 | "AllowedOrigins": [ 14 | "https://localhost:5000", 15 | "https://localhost:7247" 16 | ], 17 | "Logging": { 18 | "LogLevel": { 19 | "Default": "Information", 20 | "Microsoft": "Warning", 21 | "Microsoft.Hosting.Lifetime": "Information" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/appsettings.Docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationDatabase": "Server=localhost;Port=5432;Database=postgres;User Id=postgres;Password=password;" 4 | }, 5 | "Elasticsearch": { 6 | "Uri": "https://es01:9200", 7 | "IndexName": "documents", 8 | "Username": "elastic", 9 | "Password": "secret", 10 | "CertificateFingerprint": "31a63ffca5275df7ea7d6fc7e92b42cfa774a0feed7d7fa8488c5e46ea9ade3f" 11 | }, 12 | "AllowedHosts": "localhost", 13 | "AllowedOrigins": [ 14 | "https://localhost:5000", 15 | "https://localhost:7247" 16 | ], 17 | "Logging": { 18 | "LogLevel": { 19 | "Default": "Information", 20 | "Microsoft": "Warning", 21 | "Microsoft.Hosting.Lifetime": "Information" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationDatabase": "Server=localhost;Port=5432;Database=postgres;User Id=postgres;Password=password;" 4 | }, 5 | "Application": { 6 | "BaseUri": "https://localhost:5000" 7 | }, 8 | "Elasticsearch": { 9 | "Uri": "https://localhost:9200", 10 | "IndexName": "documents", 11 | "Username": "elastic", 12 | "Password": "secret", 13 | "CertificateFingerprint": "31a63ffca5275df7ea7d6fc7e92b42cfa774a0feed7d7fa8488c5e46ea9ade3f" 14 | }, 15 | "Logging": { 16 | "LogLevel": { 17 | "Default": "Information", 18 | "Microsoft": "Warning", 19 | "Microsoft.Hosting.Lifetime": "Information" 20 | } 21 | }, 22 | "AllowedHosts": "localhost", 23 | "AllowedOrigins": [ 24 | "https://localhost:5000", 25 | "https://localhost:7046" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/ElasticsearchFulltextExample.Database.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/Document.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Database.Model 4 | { 5 | public class Document : Entity 6 | { 7 | /// 8 | /// Gets or sets the title. 9 | /// 10 | public required string Title { get; set; } 11 | 12 | /// 13 | /// Gets or sets the original filename. 14 | /// 15 | public required string Filename { get; set; } 16 | 17 | /// 18 | /// Gets or sets the data. 19 | /// 20 | public required byte[] Data { get; set; } 21 | 22 | /// 23 | /// Gets or sets the upload date. 24 | /// 25 | public DateTime UploadedAt { get; set; } = DateTime.UtcNow; 26 | 27 | /// 28 | /// Gets or sets the index date. 29 | /// 30 | public DateTime? IndexedAt { get; set; } = null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/DocumentKeyword.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Database.Model 4 | { 5 | /// 6 | /// Association between a Document and a Keyword. 7 | /// 8 | public class DocumentKeyword : Entity 9 | { 10 | /// 11 | /// Gets or sets the DocumentId. 12 | /// 13 | public required int DocumentId { get; set; } 14 | 15 | /// 16 | /// Gets or sets the KeywordId. 17 | /// 18 | public required int KeywordId { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/DocumentSuggestion.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Database.Model 4 | { 5 | /// 6 | /// Association between a Document and a Keyword. 7 | /// 8 | public class DocumentSuggestion : Entity 9 | { 10 | /// 11 | /// Gets or sets the DocumentId. 12 | /// 13 | public required int DocumentId { get; set; } 14 | 15 | /// 16 | /// Gets or sets the SuggestionId. 17 | /// 18 | public required int SuggestionId { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/Entity.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using NodaTime; 4 | 5 | namespace ElasticsearchFulltextExample.Database.Model 6 | { 7 | public abstract class Entity 8 | { 9 | /// 10 | /// Gets or sets the Id. 11 | /// 12 | public int Id { get; set; } 13 | 14 | /// 15 | /// Gets or sets the user the entity row version. 16 | /// 17 | public uint? RowVersion { get; set; } 18 | 19 | /// 20 | /// Gets or sets the user, that made the latest modifications. 21 | /// 22 | public required int LastEditedBy { get; set; } 23 | 24 | /// 25 | /// Gets or sets the SysPeriod. 26 | /// 27 | public Interval? SysPeriod { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/Keyword.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Database.Model 4 | { 5 | /// 6 | /// A Keyword. 7 | /// 8 | public class Keyword : Entity 9 | { 10 | /// 11 | /// Gets or sets the Name. 12 | /// 13 | public required string Name { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/OutboxEvent.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Text.Json; 4 | 5 | namespace ElasticsearchFulltextExample.Database.Model 6 | { 7 | /// 8 | /// Outbox Events. 9 | /// 10 | public class OutboxEvent : Entity 11 | { 12 | /// 13 | /// Gets or sets an optional Correlation ID. 14 | /// 15 | public string? CorrelationId1 { get; set; } 16 | 17 | /// 18 | /// Gets or sets an optional Correlation ID. 19 | /// 20 | public string? CorrelationId2 { get; set; } 21 | 22 | /// 23 | /// Gets or sets an optional Correlation ID. 24 | /// 25 | public string? CorrelationId3 { get; set; } 26 | 27 | /// 28 | /// Gets or sets an optional Correlation ID. 29 | /// 30 | public string? CorrelationId4 { get; set; } 31 | 32 | /// 33 | /// Gets or sets the type Event. 34 | /// 35 | public required string EventType { get; set; } 36 | 37 | /// 38 | /// Gets or sets the source of the event. 39 | /// 40 | public string EventSource { get; set; } = "FTS"; 41 | 42 | /// 43 | /// The time (in UTC) the event was generated. 44 | /// 45 | public DateTimeOffset EventTime { get; set; } = DateTimeOffset.UtcNow; 46 | 47 | /// 48 | /// Gets or sets the Events Payload. 49 | /// 50 | public required JsonDocument Payload { get; set; } 51 | } 52 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/Suggestion.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Database.Model 4 | { 5 | /// 6 | /// A Suggestion. 7 | /// 8 | public class Suggestion : Entity 9 | { 10 | /// 11 | /// Gets or sets the Name. 12 | /// 13 | public required string Name { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Model/User.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Database.Model 4 | { 5 | public class User : Entity 6 | { 7 | /// 8 | /// Gets or sets the Email. 9 | /// 10 | public required string Email { get; set; } 11 | 12 | /// 13 | /// Gets or sets the PreferredName. 14 | /// 15 | public required string PreferredName { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Database/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:9000", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | "ElasticsearchFulltextExample.Web": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "applicationUrl": "http://localhost:5000" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Client/ApiException.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Net; 4 | using System.Runtime.Serialization; 5 | 6 | namespace ElasticsearchFulltextExample.Shared.Client 7 | { 8 | public class ApiException : Exception 9 | { 10 | public ApiException() 11 | { 12 | } 13 | 14 | public ApiException(string? message) : base(message) 15 | { 16 | } 17 | 18 | public ApiException(string? message, Exception? innerException) : base(message, innerException) 19 | { 20 | } 21 | 22 | /// 23 | /// Http status code. 24 | /// 25 | public required HttpStatusCode StatusCode { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Constants/FileUploadNames.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Shared.Constants 4 | { 5 | /// 6 | /// Field Names used in File Uploads. 7 | /// 8 | public static class FileUploadNames 9 | { 10 | /// 11 | /// Title. 12 | /// 13 | public const string Title = "title"; 14 | 15 | /// 16 | /// Suggestions. 17 | /// 18 | public const string Suggestions = "suggestions"; 19 | 20 | /// 21 | /// Keywords. 22 | /// 23 | public const string Keywords = "keywords"; 24 | 25 | /// 26 | /// Data. 27 | /// 28 | public const string Data = "data"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/ElasticsearchFulltextExample.Shared.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Models/SearchRequestDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace ElasticsearchFulltextExample.Shared.Models 7 | { 8 | public class SearchRequestDto 9 | { 10 | [JsonPropertyName("query")] 11 | public required string Query { get; set; } 12 | 13 | [JsonPropertyName("from")] 14 | public required int From { get; set; } 15 | 16 | [JsonPropertyName("to")] 17 | public required int To { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Models/SearchResultDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace ElasticsearchFulltextExample.Shared.Models 7 | { 8 | public class SearchResultDto 9 | { 10 | [JsonPropertyName("identifier")] 11 | public required string Identifier { get; set; } 12 | 13 | [JsonPropertyName("title")] 14 | public required string Title { get; set; } 15 | 16 | [JsonPropertyName("filename")] 17 | public required string Filename { get; set; } 18 | 19 | [JsonPropertyName("matches")] 20 | public List Matches { get; set; } = new(); 21 | 22 | [JsonPropertyName("keywords")] 23 | public List Keywords { get; set; } = new(); 24 | 25 | [JsonPropertyName("url")] 26 | public required string? Url { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Models/SearchResultsDto.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Philipp Wagner. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace ElasticsearchFulltextExample.Shared.Models 7 | { 8 | public class SearchResultsDto 9 | { 10 | [JsonPropertyName("query")] 11 | public required string Query { get; set; } 12 | 13 | [JsonPropertyName("from")] 14 | public required int From { get; set; } 15 | 16 | [JsonPropertyName("size")] 17 | public required int Size { get; set; } 18 | 19 | [JsonPropertyName("tookInMilliseconds")] 20 | public required long TookInMilliseconds { get; set; } 21 | 22 | [JsonPropertyName("total")] 23 | public required long Total { get; set; } 24 | 25 | [JsonPropertyName("results")] 26 | public List Results { get; set; } = []; 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Models/SearchSuggestionDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Shared.Models 4 | { 5 | public class SearchSuggestionDto 6 | { 7 | [JsonPropertyName("text")] 8 | public required string Text { get; set; } 9 | 10 | 11 | [JsonPropertyName("highlight")] 12 | public required string Highlight { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Shared/Models/SearchSuggestionsDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace ElasticsearchFulltextExample.Shared.Models 4 | { 5 | public class SearchSuggestionsDto 6 | { 7 | [JsonPropertyName("query")] 8 | public required string Query { get; set; } 9 | 10 | 11 | [JsonPropertyName("results")] 12 | public List Results { get; set; } = []; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/Autocomplete/Autocomplete.razor.css: -------------------------------------------------------------------------------- 1 | .autocomplete-root { 2 | position: relative; 3 | display: inline-block; 4 | width: 100%; 5 | } 6 | 7 | .autocomplete-input { 8 | width: 100%; 9 | } 10 | 11 | .autocomplete-panel { 12 | position: absolute; 13 | top: 40px; 14 | left: 0px; 15 | display: inline-block; 16 | background-color: #fff; 17 | border-radius: 4px; 18 | border: 1px solid #dadfe2; 19 | border-top-right-radius: 0; 20 | border-top-left-radius: 0; 21 | overflow: auto; 22 | box-sizing: content-box; 23 | margin-top: 1px; 24 | max-height: 150px; 25 | width: 100%; 26 | z-index: 99; 27 | } 28 | 29 | .autocomplete-item { 30 | padding: 0.25rem 0.625rem; 31 | display: block; 32 | width: 100%; 33 | clear: both; 34 | font-weight: 400; 35 | text-align: inherit; 36 | } 37 | /*Hover via mouse pointer*/ 38 | .autocomplete-item:hover { 39 | background-color: #3ba5fc; 40 | color: white; 41 | } 42 | 43 | /*Hover via code (on arrow up/down)*/ 44 | .autocomplete-item-hover { 45 | background-color: #3ba5fc; 46 | color: white; 47 | } 48 | 49 | .autocomplete-items { 50 | list-style: none; 51 | padding: 0; 52 | display: block; 53 | width: 100%; 54 | clear: both; 55 | font-weight: 400; 56 | color: inherit; 57 | text-align: inherit; 58 | } 59 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/Autocomplete/AutocompleteItem.cs: -------------------------------------------------------------------------------- 1 | namespace ElasticsearchFulltextExample.Web.Client.Components 2 | { 3 | public class AutocompleteItem 4 | { 5 | public required string Html { get; set; } = string.Empty; 6 | 7 | public required string Text { get; set; } = string.Empty; 8 | 9 | public bool IsSelected { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/Autocomplete/AutocompleteSearchEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace ElasticsearchFulltextExample.Web.Client.Components 2 | { 3 | public class AutocompleteSearchEventArgs 4 | { 5 | /// 6 | /// Gets or sets the text to search. 7 | /// 8 | public string Text { get; set; } = string.Empty; 9 | 10 | /// 11 | /// Gets or sets the list of items to display. 12 | /// 13 | public List? Items { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/NotificationCenter/NotificationCenter.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | @inject IDialogService DialogService 3 | @inject IMessageService MessageService 4 | 5 | @namespace ElasticsearchFulltextExample.Web.Client.Components 6 | 7 | 8 | @if (MessageService.Count(Routes.MESSAGES_NOTIFICATION_CENTER) > 0) 9 | { 10 | 17 | 18 | @NotificationIcon() 19 | 20 | 21 | } 22 | else 23 | { 24 | @NotificationIcon() 25 | } 26 | 27 | 28 | @code { 29 | private IDialogReference? _dialog; 30 | 31 | protected override void OnInitialized() 32 | { 33 | MessageService.OnMessageItemsUpdated += UpdateCount; 34 | } 35 | 36 | private void UpdateCount() 37 | { 38 | InvokeAsync(StateHasChanged); 39 | } 40 | 41 | private RenderFragment NotificationIcon() => 42 | @; 43 | 44 | private async Task OpenNotificationCenterAsync() 45 | { 46 | _dialog = await DialogService.ShowPanelAsync(new DialogParameters() 47 | { 48 | Alignment = HorizontalAlignment.Right, 49 | Title = $"Notifications", 50 | PrimaryAction = null, 51 | SecondaryAction = null, 52 | ShowDismiss = true 53 | }); 54 | DialogResult result = await _dialog.Result; 55 | HandlePanel(result); 56 | } 57 | 58 | private static void HandlePanel(DialogResult result) 59 | { 60 | if (result.Cancelled) 61 | { 62 | return; 63 | } 64 | 65 | if (result.Data is not null) 66 | { 67 | return; 68 | } 69 | } 70 | 71 | public void Dispose() 72 | { 73 | MessageService.OnMessageItemsUpdated -= UpdateCount; 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/NotificationCenter/NotificationCenterPanel.razor: -------------------------------------------------------------------------------- 1 | @implements IDialogContentComponent 2 | @inject IMessageService MessageService 3 | 4 | @namespace ElasticsearchFulltextExample.Web.Client.Components 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | Dismiss all 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | @code { 22 | [Parameter] 23 | public GlobalState Content { get; set; } = default!; 24 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/Paginator/Paginator.razor: -------------------------------------------------------------------------------- 1 | @namespace ElasticsearchFulltextExample.Web.Client.Components 2 | 3 | @inherits FluentComponentBase 4 |
5 | @if (State.TotalItemCount.HasValue) 6 | { 7 | 24 | } 25 |
-------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/Paginator/Paginator.razor.css: -------------------------------------------------------------------------------- 1 | .paginator { 2 | display: flex; 3 | /*border-top: 1px solid var(--neutral-stroke-divider-rest);*/ 4 | margin-top: 0.5rem; 5 | padding: 0.25rem 0; 6 | align-items: center; 7 | } 8 | 9 | .pagination-text { 10 | margin: 0 0.5rem; 11 | } 12 | 13 | .paginator-nav { 14 | padding: 0; 15 | display: flex; 16 | margin-inline-start: auto; 17 | margin-inline-end: 0; 18 | gap: 0.5rem; 19 | align-items: center; 20 | } 21 | 22 | 23 | [dir="rtl"] * ::deep fluent-button > svg { 24 | transform: rotate(180deg); 25 | } 26 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/Paginator/PaginatorState.cs: -------------------------------------------------------------------------------- 1 | using ElasticsearchFulltextExample.Web.Client.Infrastructure; 2 | 3 | namespace ElasticsearchFulltextExample.Web.Client.Components 4 | { 5 | /// 6 | /// Holds state to represent pagination in a . 7 | /// 8 | public class PaginatorState 9 | { 10 | /// 11 | /// Gets or sets the number of items on each page. 12 | /// 13 | public int ItemsPerPage { get; set; } = 10; 14 | 15 | /// 16 | /// Gets the current zero-based page index. To set it, call . 17 | /// 18 | public int CurrentPageIndex { get; private set; } 19 | 20 | /// 21 | /// Gets the total number of items across all pages, if known. The value will be null until an 22 | /// associated assigns a value after loading data. 23 | /// 24 | public int? TotalItemCount { get; private set; } 25 | 26 | /// 27 | /// Gets the zero-based index of the last page, if known. The value will be null until is known. 28 | /// 29 | public int? LastPageIndex => (TotalItemCount - 1) / ItemsPerPage; 30 | 31 | /// 32 | /// An event that is raised when the total item count has changed. 33 | /// 34 | public event EventHandler? TotalItemCountChanged; 35 | 36 | internal EventCallbackSubscribable CurrentPageItemsChanged { get; } = new(); 37 | internal EventCallbackSubscribable TotalItemCountChangedSubscribable { get; } = new(); 38 | 39 | /// 40 | public override int GetHashCode() 41 | => HashCode.Combine(ItemsPerPage, CurrentPageIndex, TotalItemCount); 42 | 43 | /// 44 | /// Sets the current page index, and notifies any associated 45 | /// to fetch and render updated data. 46 | /// 47 | /// The new, zero-based page index. 48 | /// A representing the completion of the operation. 49 | public Task SetCurrentPageIndexAsync(int pageIndex) 50 | { 51 | CurrentPageIndex = pageIndex; 52 | return CurrentPageItemsChanged.InvokeCallbacksAsync(this); 53 | } 54 | 55 | public Task SetTotalItemCountAsync(int totalItemCount) 56 | { 57 | if (totalItemCount == TotalItemCount) 58 | { 59 | return Task.CompletedTask; 60 | } 61 | 62 | TotalItemCount = totalItemCount; 63 | 64 | if (CurrentPageIndex > 0 && CurrentPageIndex > LastPageIndex) 65 | { 66 | // If the number of items has reduced such that the current page index is no longer valid, move 67 | // automatically to the final valid page index and trigger a further data load. 68 | SetCurrentPageIndexAsync(LastPageIndex.Value); 69 | } 70 | 71 | // Under normal circumstances, we just want any associated pagination UI to update 72 | TotalItemCountChanged?.Invoke(this, TotalItemCount); 73 | return TotalItemCountChangedSubscribable.InvokeCallbacksAsync(this); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/SearchResult/SearchResult.razor.css: -------------------------------------------------------------------------------- 1 | .search-result { 2 | display: grid; 3 | grid-gap: 10px; 4 | grid-template-columns: 100px 1fr; 5 | padding: 10px; 6 | width: 100%; 7 | background-color: #fff; 8 | border: 1px solid black; 9 | } 10 | 11 | .search-result .document-file-icon { 12 | grid-column: 1; 13 | grid-row: 1 / span 2; 14 | justify-content: center; 15 | } 16 | 17 | .search-result .document-file-icon img { 18 | width: 50px; 19 | } 20 | 21 | .search-result .document-title { 22 | grid-column: 2; 23 | grid-row: 1; 24 | } 25 | 26 | .search-result .document-highlight { 27 | grid-column: 2; 28 | grid-row: 2; 29 | } 30 | 31 | .search-result .document-highlight ul { 32 | grid-column: 2; 33 | grid-row: 2; 34 | margin: 0; 35 | } 36 | 37 | .search-result .document-filename { 38 | grid-column: 2; 39 | grid-row: 3; 40 | } 41 | 42 | .search-result .document-filename .filename { 43 | color: seagreen; 44 | } 45 | 46 | .search-result .document-keywords { 47 | grid-column: 2; 48 | grid-row: 4; 49 | justify-content: end; 50 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/SiteSettings/SiteSettings.razor: -------------------------------------------------------------------------------- 1 | @namespace ElasticsearchFulltextExample.Web.Client.Components 2 | 3 | @inject IDialogService DialogService 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/SiteSettings/SiteSettings.razor.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.FluentUI.AspNetCore.Components; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Components; 6 | 7 | public partial class SiteSettings 8 | { 9 | private IDialogReference? _dialog; 10 | 11 | private async Task OpenSiteSettingsAsync() 12 | { 13 | 14 | _dialog = await DialogService.ShowPanelAsync(new DialogParameters() 15 | { 16 | ShowTitle = true, 17 | Title = "Site settings", 18 | Alignment = HorizontalAlignment.Right, 19 | PrimaryAction = "OK", 20 | SecondaryAction = null, 21 | ShowDismiss = true 22 | }); 23 | 24 | DialogResult result = await _dialog.Result; 25 | } 26 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/SiteSettings/SiteSettingsPanel.razor: -------------------------------------------------------------------------------- 1 | @namespace ElasticsearchFulltextExample.Web.Client.Components 2 | @using Microsoft.FluentUI.AspNetCore.Components.Extensions 3 | @implements IDialogContentComponent 4 | 5 |
6 | 10 | 11 | 12 | 16 | 17 | 22 | 23 | 24 | 27 | @context 28 | 29 | 30 | 31 | 32 | 37 | 38 | These values (except for Direction) are persisted in the LocalStorage. 39 | You can recover this style during your next visits. 40 | 41 | 42 |
-------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/SiteSettings/SiteSettingsPanel.razor.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.FluentUI.AspNetCore.Components; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Components 6 | { 7 | public partial class SiteSettingsPanel 8 | { 9 | public DesignThemeModes Mode { get; set; } 10 | 11 | public OfficeColor? OfficeColor { get; set; } 12 | 13 | public bool Direction { get; set; } = true; 14 | 15 | private IEnumerable AllModes => Enum.GetValues(); 16 | 17 | private IEnumerable AllOfficeColors 18 | { 19 | get 20 | { 21 | return Enum.GetValues().Select(i => (OfficeColor?)i).Union(new[] { (OfficeColor?)null }); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Components/TokenInput/TokenInput.razor.css: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/ElasticsearchFulltextExample.Web.Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Default 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | true 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Extensions 6 | { 7 | public static class StringExtensions 8 | { 9 | public static MarkupString? AsMarkupString(this string? source) 10 | { 11 | if(source == null) 12 | { 13 | return null; 14 | } 15 | 16 | return (MarkupString?)source; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Extensions/StringLocalizerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.Extensions.Localization; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Extensions 6 | { 7 | public static class StringLocalizerExtensions 8 | { 9 | public static string TranslateEnum(this IStringLocalizer localizer, TEnum enumValue) 10 | { 11 | var key = $"{typeof(TEnum).Name}_{enumValue}"; 12 | 13 | var res = localizer.GetString(key); 14 | 15 | return res; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Infrastructure/ApplicationErrorMessageService.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.Extensions.Localization; 5 | using Microsoft.FluentUI.AspNetCore.Components; 6 | using ElasticsearchFulltextExample.Web.Client.Localization; 7 | 8 | namespace ElasticsearchFulltextExample.Web.Client.Infrastructure 9 | { 10 | public class ApplicationErrorMessageService 11 | { 12 | private readonly IStringLocalizer _sharedLocalizer; 13 | private readonly ApplicationErrorTranslator _applicationErrorTranslator; 14 | private readonly IMessageService _messageService; 15 | private readonly NavigationManager _navigationManager; 16 | 17 | public ApplicationErrorMessageService(IStringLocalizer sharedLocalizer, IMessageService messageService, NavigationManager navigationManager, ApplicationErrorTranslator applicationErrorTranslator) 18 | { 19 | _sharedLocalizer = sharedLocalizer; 20 | _navigationManager = navigationManager; 21 | _applicationErrorTranslator = applicationErrorTranslator; 22 | _messageService = messageService; 23 | } 24 | 25 | public void ShowErrorMessage(Exception exception, Action? configure = null) 26 | { 27 | (var errorCode, var errorMessage) = _applicationErrorTranslator.GetErrorMessage(exception); 28 | 29 | _messageService.ShowMessageBar(options => 30 | { 31 | options.Section = Routes.MESSAGES_TOP; 32 | options.Intent = MessageIntent.Error; 33 | options.ClearAfterNavigation = false; 34 | options.Title = _sharedLocalizer["Message_Error_Title"]; 35 | options.Body = errorMessage; 36 | options.Timestamp = DateTime.Now; 37 | options.Link = new ActionLink 38 | { 39 | Text = _sharedLocalizer["Message_ShowHelp"], 40 | OnClick = (message) => 41 | { 42 | _navigationManager.NavigateTo($"https://www.bytefish.de"); 43 | 44 | return Task.CompletedTask; 45 | } 46 | }; 47 | 48 | // If we need to customize it like using a different section or intent, we should 49 | // use the action passed to us ... 50 | if (configure != null) 51 | { 52 | configure(options); 53 | } 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Infrastructure/ApplicationErrorTranslator.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Web.Client.Localization; 4 | using Microsoft.Extensions.Localization; 5 | 6 | namespace ElasticsearchFulltextExample.Web.Client.Infrastructure 7 | { 8 | public class ApplicationErrorTranslator 9 | { 10 | private readonly IStringLocalizer _sharedLocalizer; 11 | 12 | public ApplicationErrorTranslator(IStringLocalizer sharedLocalizer) 13 | { 14 | _sharedLocalizer = sharedLocalizer; 15 | } 16 | 17 | public (string ErrorCode, string ErrorMessage) GetErrorMessage(Exception exception) 18 | { 19 | return exception switch 20 | { 21 | Exception e => (LocalizationConstants.ClientError_UnexpectedError, GetErrorMessageFromException(e)), 22 | }; 23 | } 24 | 25 | private string GetErrorMessageFromException(Exception e) 26 | { 27 | string errorMessage = _sharedLocalizer["ApplicationError_Exception"]; 28 | 29 | return errorMessage; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Infrastructure/EventCallbackSubscribable.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Infrastructure 6 | { 7 | public sealed class EventCallbackSubscribable 8 | { 9 | private readonly Dictionary, EventCallback> _callbacks = new(); 10 | 11 | /// 12 | /// Invokes all the registered callbacks sequentially, in an undefined order. 13 | /// 14 | public async Task InvokeCallbacksAsync(T eventArg) 15 | { 16 | foreach (var callback in _callbacks.Values) 17 | { 18 | await callback.InvokeAsync(eventArg); 19 | } 20 | } 21 | 22 | // Don't call this directly - it gets called by EventCallbackSubscription 23 | public void Subscribe(EventCallbackSubscriber owner, EventCallback callback) 24 | => _callbacks.Add(owner, callback); 25 | 26 | // Don't call this directly - it gets called by EventCallbackSubscription 27 | public void Unsubscribe(EventCallbackSubscriber owner) 28 | => _callbacks.Remove(owner); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Infrastructure/EventCallbackSubscriber.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Infrastructure 6 | { 7 | public sealed class EventCallbackSubscriber : IDisposable 8 | { 9 | private readonly EventCallback _handler; 10 | private EventCallbackSubscribable? _existingSubscription; 11 | 12 | public EventCallbackSubscriber(EventCallback handler) 13 | { 14 | _handler = handler; 15 | } 16 | 17 | /// 18 | /// Creates a subscription on the , or moves any existing subscription to it 19 | /// by first unsubscribing from the previous . 20 | /// 21 | /// If the supplied is null, no new subscription will be created, but any 22 | /// existing one will still be unsubscribed. 23 | /// 24 | /// 25 | public void SubscribeOrMove(EventCallbackSubscribable? subscribable) 26 | { 27 | if (subscribable != _existingSubscription) 28 | { 29 | _existingSubscription?.Unsubscribe(this); 30 | subscribable?.Subscribe(this, _handler); 31 | _existingSubscription = subscribable; 32 | } 33 | } 34 | 35 | public void Dispose() 36 | { 37 | _existingSubscription?.Unsubscribe(this); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Infrastructure/StringLocalizerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.Extensions.Localization; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Infrastructure 6 | { 7 | public static class StringLocalizerExtensions 8 | { 9 | public static string TranslateEnum(this IStringLocalizer localizer, TEnum enumValue) 10 | { 11 | var key = $"{typeof(TEnum).Name}_{enumValue}"; 12 | 13 | var res = localizer.GetString(key); 14 | 15 | return res; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Infrastructure/TimeFormattingUtils.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Globalization; 4 | 5 | namespace ElasticsearchFulltextExample.Web.Client.Infrastructure 6 | { 7 | public static class TimeFormattingUtils 8 | { 9 | public static string MillisecondsToSeconds(long? milliseconds, string defaultValue) 10 | { 11 | if(!milliseconds.HasValue) 12 | { 13 | return defaultValue; 14 | } 15 | 16 | var timeSpan = TimeSpan.FromMilliseconds(milliseconds.Value); 17 | 18 | return timeSpan.TotalSeconds.ToString("F"); 19 | } 20 | 21 | public static string MillisecondsToSeconds(long? milliseconds, string defaultValue, CultureInfo cultureInfo) 22 | { 23 | if (!milliseconds.HasValue) 24 | { 25 | return defaultValue; 26 | } 27 | 28 | var timeSpan = TimeSpan.FromMilliseconds(milliseconds.Value); 29 | 30 | return timeSpan.TotalSeconds.ToString("F", cultureInfo); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Localization/LocalizationConstants.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Web.Client.Localization 4 | { 5 | public static class LocalizationConstants 6 | { 7 | public const string ClientError_UnexpectedError = "ClientError_UnexpectedError"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Localization/SharedResource.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Web.Client.Localization 4 | { 5 | public class SharedResource 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Models/ElasticsearchIndexMetrics.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Web.Client.Models 4 | { 5 | public class ElasticsearchIndexMetrics 6 | { 7 | /// 8 | /// Index. 9 | /// 10 | public required string Index { get; set; } 11 | 12 | /// 13 | /// Value. 14 | /// 15 | public required List Metrics { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Models/ElasticsearchMetric.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Web.Client.Models 4 | { 5 | public class ElasticsearchMetric 6 | { 7 | /// 8 | /// Name. 9 | /// 10 | public required string Name { get; set; } 11 | 12 | /// 13 | /// Elasticsearch Key. 14 | /// 15 | public required string Key { get; set; } 16 | 17 | /// 18 | /// Value. 19 | /// 20 | public required string? Value { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Models/SortOptionEnum.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | namespace ElasticsearchFulltextExample.Web.Client.Models 4 | { 5 | /// 6 | /// Sort Options for sorting search results. 7 | /// 8 | public enum SortOptionEnum 9 | { 10 | /// 11 | /// Sorts by Owner in ascending order. 12 | /// 13 | OwnerAscending = 1, 14 | 15 | /// 16 | /// Sorts by Owner in descending order. 17 | /// 18 | OwnerDescending = 2, 19 | 20 | /// 21 | /// Sorts by Repository in ascending order. 22 | /// 23 | RepositoryAscending = 3, 24 | 25 | /// 26 | /// Sorts by Respository in ascending order. 27 | /// 28 | RepositoryDescending = 4, 29 | 30 | /// 31 | /// Sorts by Latest Commit Date in ascending order. 32 | /// 33 | LatestCommitDateAscending = 5, 34 | 35 | /// 36 | /// Sorts by Latest Commit Date in descending order. 37 | /// 38 | LatestCommitDateDescending = 6, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Search Cluster Overview 4 | 5 |

Search Cluster Overview

6 | 7 |

8 | This page gives you an overview for all indices in your Elasticsearch cluster. 9 |

10 | 11 | @foreach (var indexMetric in _elasticsearchIndexMetrics) 12 | { 13 |

Index "@indexMetric.Index"

14 | 15 | 16 | 17 | 18 | 19 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Index.razor.css: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Manage.razor: -------------------------------------------------------------------------------- 1 | @page "/Manage" 2 | 3 | @using ElasticsearchFulltextExample.Web.Client.Components 4 | @using ElasticsearchFulltextExample.Web.Client.Extensions; 5 | @using ElasticsearchFulltextExample.Web.Client.Infrastructure; 6 | @using ElasticsearchFulltextExample.Shared.Client 7 | 8 | @inject SearchClient SearchClient 9 | @inject IStringLocalizer Loc 10 | 11 | Manage Search Index (Debugging) 12 | 13 | 14 | @Loc["ManageSearchIndex_CreateSearchIndex"] 15 | @Loc["ManageSearchIndex_DeleteSearchIndex"] 16 | @Loc["ManageSearchIndex_RecreateSearchPipeline"] 17 | @Loc["ManageSearchIndex_RecreateSearchIndex"] 18 | @Loc["ManageSearchIndex_DeleteAllDocuments"] 19 | 20 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Manage.razor.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Shared.Client; 4 | using ElasticsearchFulltextExample.Shared.Constants; 5 | 6 | namespace ElasticsearchFulltextExample.Web.Client.Pages 7 | { 8 | public partial class Manage 9 | { 10 | /// 11 | /// Recreates the Search Index. 12 | /// 13 | /// An awaitable 14 | private async Task HandleRecreateSearchIndexAsync() 15 | { 16 | await SearchClient.DeleteSearchPipelineAsync(default); 17 | await SearchClient.DeleteSearchIndexAsync(default); 18 | 19 | await SearchClient.CreateSearchIndexAsync(default); 20 | await SearchClient.CreateSearchPipelineAsync(default); 21 | } 22 | 23 | /// 24 | /// Recreates the Search Pipeline. 25 | /// 26 | /// An awaitable 27 | private async Task HandleRecreateSearchPipelineAsync() 28 | { 29 | await SearchClient.DeleteSearchPipelineAsync(default); 30 | await SearchClient.CreateSearchIndexAsync(default); 31 | } 32 | 33 | /// 34 | /// Creates the Search Index. 35 | /// 36 | /// An awaitable 37 | private async Task HandleCreateSearchIndexAsync() 38 | { 39 | await SearchClient.CreateSearchIndexAsync(default); 40 | } 41 | 42 | /// 43 | /// Deletes the Search Index. 44 | /// 45 | /// An awaitable 46 | private async Task HandleDeleteSearchIndexAsync() 47 | { 48 | await SearchClient.DeleteSearchIndexAsync(default); 49 | } 50 | 51 | /// 52 | /// Deletes all Documents from Index. 53 | /// 54 | /// An awaitable 55 | private async Task HandleDeleteAllDocumentsAsync() 56 | { 57 | await SearchClient.DeleteAllDocumentsAsync(default); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Search.razor: -------------------------------------------------------------------------------- 1 | @page "/Search" 2 | 3 | @using ElasticsearchFulltextExample.Shared.Client 4 | @using ElasticsearchFulltextExample.Web.Client.Components 5 | @using ElasticsearchFulltextExample.Web.Client.Extensions; 6 | @using ElasticsearchFulltextExample.Web.Client.Infrastructure; 7 | 8 | @inject SearchClient SearchClient 9 | 10 | Search indexed documents 11 | 12 |
13 |
14 | 18 |
19 |
20 | @_totalItemCount Results (@_tookInSeconds seconds) 21 |
22 |
23 | 24 | @foreach (var searchResult in _searchResults) 25 | { 26 | 27 | } 28 | 29 | 30 |
31 |
32 | 33 |
34 |
-------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Search.razor.css: -------------------------------------------------------------------------------- 1 | .search-container { 2 | display: grid; 3 | height: 100%; 4 | grid-template-rows: auto auto 1fr auto; 5 | grid-template-columns: 1fr; 6 | grid-row-gap: 10px; 7 | grid-template-areas: 8 | "search-header" 9 | "search-results-total" 10 | "search-results" 11 | "search-paginator" 12 | } 13 | 14 | .search-header { 15 | display: grid; 16 | grid-area: search-header; 17 | grid-template-columns: minmax(auto, 900px); 18 | grid-template-rows: auto 1fr; 19 | justify-content: center; 20 | padding: 1rem; 21 | border-bottom: 1px solid var(--neutral-foreground-rest); 22 | } 23 | 24 | .search-header .search-title { 25 | color: black; 26 | } 27 | 28 | .search-title { 29 | display: grid; 30 | justify-content: center; 31 | } 32 | 33 | .search-box { 34 | display: grid; 35 | min-width: 500px; 36 | justify-content: center; 37 | grid-template-columns: 1fr auto auto; 38 | grid-column-gap: 10px; 39 | } 40 | 41 | .search-results-total { 42 | display: grid; 43 | grid-area: search-results-total; 44 | justify-content: center; 45 | grid-template-columns: auto; 46 | } 47 | 48 | .search-results { 49 | display: grid; 50 | grid-area: search-results; 51 | grid-template-columns: 1fr; 52 | grid-auto-rows: max-content; 53 | max-width: 1000px; 54 | margin: 0 auto; 55 | grid-row-gap: 20px; 56 | width: 100%; 57 | } 58 | 59 | .search-paginator { 60 | display: grid; 61 | grid-area: search-paginator; 62 | min-width: 500px; 63 | justify-content: center; 64 | grid-template-columns: auto; 65 | } 66 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Pages/Upload.razor.css: -------------------------------------------------------------------------------- 1 | .upload-form { 2 | display: grid; 3 | grid-template-columns: 100px 1fr 150px; 4 | border: 1px solid black; 5 | } 6 | 7 | .upload-form .upload-label { 8 | grid-column: 1 / 2; 9 | align-content: center; 10 | padding: 10px; 11 | border-right: 1px solid black; 12 | border-bottom: 1px solid black; 13 | } 14 | 15 | .upload-form .upload-input { 16 | grid-column: 2 / 3; 17 | align-content: center; 18 | padding: 10px; 19 | border-bottom: 1px solid black; 20 | } 21 | 22 | .upload-form .upload-validation { 23 | grid-column: 3 / 4; 24 | align-content: center; 25 | padding: 10px; 26 | border-bottom: 1px solid black; 27 | } 28 | 29 | .upload-form .upload-buttons { 30 | grid-column: 1 / 4; 31 | padding: 10px; 32 | } 33 | 34 | .w-100 { 35 | width: 100%; 36 | } 37 | 38 | .form-input { 39 | width: 200px; 40 | } 41 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Program.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | using Microsoft.FluentUI.AspNetCore.Components; 5 | using ElasticsearchFulltextExample.Shared.Client; 6 | using ElasticsearchFulltextExample.Web.Client.Infrastructure; 7 | 8 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 9 | 10 | builder.Services.AddScoped(); 11 | builder.Services.AddScoped(); 12 | 13 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 14 | 15 | builder.Services.AddHttpClient((services, client) => 16 | { 17 | client.BaseAddress = new Uri(builder.Configuration["SearchService:BaseAddress"]!); 18 | }); 19 | 20 | builder.Services.AddLocalization(); 21 | 22 | // Fluent UI 23 | builder.Services.AddFluentUIComponents(); 24 | 25 | await builder.Build().RunAsync(); 26 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:44900", 8 | "sslPort": 44300 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5262", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7046;http://localhost:5262", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Not found 9 | 10 |

Sorry, there's nothing at this address.

11 |
12 |
13 |
-------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Routes.razor.cs: -------------------------------------------------------------------------------- 1 | namespace ElasticsearchFulltextExample.Web.Client 2 | { 3 | public partial class Routes 4 | { 5 | public const string MESSAGES_NOTIFICATION_CENTER = "NOTIFICATION_CENTER"; 6 | public const string MESSAGES_TOP = "TOP"; 7 | public const string MESSAGES_DIALOG = "DIALOG"; 8 | public const string MESSAGES_CARD = "CARD"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @using ElasticsearchFulltextExample.Web.Client.Components 2 | @using ElasticsearchFulltextExample.Web.Client 3 | @using ElasticsearchFulltextExample.Web.Client.Infrastructure 4 | @using ElasticsearchFulltextExample.Web.Client.Shared 5 | @using Microsoft.AspNetCore.Components 6 | @using System.Runtime.InteropServices 7 | 8 | @namespace ElasticsearchFulltextExample.Web.Client.Shared 9 | 10 | @inject IStringLocalizer Loc 11 | @inject ApplicationErrorMessageService ApplicationErrorMessageService 12 | 13 | Elasticsearch Fulltext Search 14 |
15 | 16 | 17 | 18 | Elasticsearch Fulltext Search 19 | 20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 |
@Body
37 |
38 | 39 | @{ 40 | ApplicationErrorMessageService.ShowErrorMessage(exception); 41 | } 42 | 43 |
44 |
45 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 55 | 56 |
57 | 58 | Version: @_version 59 |  -  60 | Powered by @RuntimeInformation.FrameworkDescription 61 | 62 |
63 | 64 | 65 |
66 | © 2023. All rights reserved. 67 |
68 |
69 | 70 |
71 |
72 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Shared/MainLayout.razor.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using System.Reflection; 4 | using Microsoft.AspNetCore.Components; 5 | using Microsoft.AspNetCore.Components.Routing; 6 | using Microsoft.AspNetCore.Components.Web; 7 | using Microsoft.JSInterop; 8 | 9 | namespace ElasticsearchFulltextExample.Web.Client.Shared 10 | { 11 | public partial class MainLayout 12 | { 13 | private const string JAVASCRIPT_FILE = "./Shared/MainLayout.razor.js"; 14 | private string? _version; 15 | private bool _mobile; 16 | private string? _prevUri; 17 | private bool _menuChecked = true; 18 | 19 | [Inject] 20 | private NavigationManager NavigationManager { get; set; } = default!; 21 | 22 | [Inject] 23 | public IJSRuntime JSRuntime { get; set; } = default!; 24 | 25 | [Parameter] 26 | public RenderFragment? Body { get; set; } 27 | 28 | private ErrorBoundary? _errorBoundary; 29 | 30 | protected override void OnInitialized() 31 | { 32 | _version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion; 33 | _prevUri = NavigationManager.Uri; 34 | NavigationManager.LocationChanged += LocationChanged; 35 | } 36 | 37 | protected override void OnParametersSet() 38 | { 39 | _errorBoundary?.Recover(); 40 | } 41 | 42 | protected override async Task OnAfterRenderAsync(bool firstRender) 43 | { 44 | if (firstRender) 45 | { 46 | var jsModule = await JSRuntime.InvokeAsync("import", JAVASCRIPT_FILE); 47 | _mobile = await jsModule.InvokeAsync("isDevice"); 48 | await jsModule.DisposeAsync(); 49 | } 50 | } 51 | 52 | private void HandleChecked() 53 | { 54 | _menuChecked = !_menuChecked; 55 | } 56 | 57 | private void LocationChanged(object? sender, LocationChangedEventArgs e) 58 | { 59 | if (!e.IsNavigationIntercepted && new Uri(_prevUri!).AbsolutePath != new Uri(e.Location).AbsolutePath) 60 | { 61 | _prevUri = e.Location; 62 | if (_mobile && _menuChecked == true) 63 | { 64 | _menuChecked = false; 65 | StateHasChanged(); 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Shared/MainLayout.razor.js: -------------------------------------------------------------------------------- 1 | export function isDevice() { 2 | return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(navigator.userAgent); 3 | } 4 | 5 | export function isDarkMode() { 6 | let matched = window.matchMedia("(prefers-color-scheme: dark)").matches; 7 | 8 | if (matched) 9 | return true; 10 | else 11 | return false; 12 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  19 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Microsoft.FluentUI.AspNetCore.Components 10 | @using Microsoft.Extensions.Localization 11 | @using ElasticsearchFulltextExample.Web.Client 12 | @using ElasticsearchFulltextExample.Web.Client.Infrastructure 13 | @using ElasticsearchFulltextExample.Web.Client.Localization 14 | @using ElasticsearchFulltextExample.Web.Client.Shared 15 | @using static Microsoft.AspNetCore.Components.Web.RenderMode -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "SearchService": { 3 | "BaseAddress": "https://localhost:5000" 4 | } 5 | } -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/HTML5_logo_and_wordmark.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/HTML5_logo_and_wordmark.svg.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/PDF_file_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/PDF_file_icon.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/Text-txt.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/Text-txt.svg.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/docx_icon.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/512/docx_icon.svg.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/HTML5_logo_and_wordmark.svg: -------------------------------------------------------------------------------- 1 | 2 | HTML5 Logo 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/JPEG_format_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JPEG 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/Markdown-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Client/wwwroot/images/docx_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | An unhandled error has occurred. 22 | Reload 23 | 🗙 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/ElasticsearchFulltextExample.Web.Server.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/Program.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 2 | 3 | using ElasticsearchFulltextExample.Web.Server; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Add services to the container. 8 | builder.Services.AddRazorPages(); 9 | 10 | builder.Services.AddRazorComponents() 11 | .AddInteractiveWebAssemblyComponents(); 12 | 13 | var app = builder.Build(); 14 | 15 | // Configure the HTTP request pipeline. 16 | if (app.Environment.IsDevelopment()) 17 | { 18 | app.UseWebAssemblyDebugging(); 19 | } 20 | else 21 | { 22 | app.UseHsts(); 23 | 24 | } 25 | 26 | app.UseHttpsRedirection(); 27 | app.UseStaticFiles(); 28 | app.MapRazorComponents() 29 | .AddInteractiveWebAssemblyRenderMode() 30 | .AddAdditionalAssemblies(typeof(ElasticsearchFulltextExample.Web.Client._Imports).Assembly); 31 | 32 | app.UseRouting(); 33 | app.UseAntiforgery(); 34 | 35 | app.Run(); 36 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:44900", 8 | "sslPort": 44300 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5262", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7046;http://localhost:5262", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Microsoft.FluentUI.AspNetCore.Components 10 | @using Microsoft.Extensions.Localization 11 | @using ElasticsearchFulltextExample.Web.Client 12 | @using ElasticsearchFulltextExample.Web.Client.Infrastructure 13 | @using ElasticsearchFulltextExample.Web.Client.Localization 14 | @using ElasticsearchFulltextExample.Web.Client.Shared 15 | @using static Microsoft.AspNetCore.Components.Web.RenderMode -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/icon-192.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-html.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-jpeg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-jpeg.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-pdf.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-svg.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extension-txt.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extensions-md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytefish/ElasticsearchFulltextExample/e612db41abc698efdf9ac1205bf2245ab5b13458/src/ElasticsearchFulltextExample.Web.Server/wwwroot/img/extensions-md.png -------------------------------------------------------------------------------- /src/ElasticsearchFulltextExample.Web.Server/wwwroot/js/theme.js: -------------------------------------------------------------------------------- 1 | import { 2 | baseLayerLuminance, 3 | StandardLuminance 4 | } from "/_content/Microsoft.FluentUI.AspNetCore.Components/js/web-components-v2.5.16.min.js"; 5 | 6 | const currentThemeCookieName = "currentTheme"; 7 | const themeSettingSystem = "System"; 8 | const themeSettingDark = "Dark"; 9 | const themeSettingLight = "Light"; 10 | 11 | /** 12 | * Returns the current system theme (Light or Dark) 13 | * @returns {string} 14 | */ 15 | export function getSystemTheme() { 16 | let matched = window.matchMedia('(prefers-color-scheme: dark)').matches; 17 | 18 | if (matched) { 19 | return themeSettingDark; 20 | } else { 21 | return themeSettingLight; 22 | } 23 | } 24 | 25 | /** 26 | * Sets the currentTheme cookie to the specified value. 27 | * @param {string} theme 28 | */ 29 | export function setThemeCookie(theme) { 30 | document.cookie = `${currentThemeCookieName}=${theme}`; 31 | } 32 | 33 | /** 34 | * Returns the value of the currentTheme cookie, or System if the cookie is not set. 35 | * @returns {string} 36 | */ 37 | export function getThemeCookieValue() { 38 | return getCookieValue(currentThemeCookieName) ?? themeSettingSystem; 39 | } 40 | 41 | export function switchHighlightStyle(dark) { 42 | if (dark) { 43 | document.querySelector(`link[title="dark"]`)?.removeAttribute("disabled"); 44 | document.querySelector(`link[title="light"]`)?.setAttribute("disabled", "disabled"); 45 | } 46 | else { 47 | document.querySelector(`link[title="light"]`)?.removeAttribute("disabled"); 48 | document.querySelector(`link[title="dark"]`)?.setAttribute("disabled", "disabled"); 49 | } 50 | } 51 | 52 | /** 53 | * Returns the value of the specified cookie, or the empty string if the cookie is not present 54 | * @param {string} cookieName 55 | * @returns {string} 56 | */ 57 | function getCookieValue(cookieName) { 58 | const cookiePieces = document.cookie.split(';'); 59 | for (let index = 0; index < cookiePieces.length; index++) { 60 | if (cookiePieces[index].trim().startsWith(cookieName)) { 61 | const cookieKeyValue = cookiePieces[index].split('='); 62 | if (cookieKeyValue.length > 1) { 63 | return cookieKeyValue[1]; 64 | } 65 | } 66 | } 67 | 68 | return ""; 69 | } 70 | 71 | function setInitialBaseLayerLuminance() { 72 | let theme = getThemeCookieValue(); 73 | 74 | if (!theme || theme === themeSettingSystem) { 75 | theme = getSystemTheme(); 76 | } 77 | 78 | if (theme === themeSettingDark) { 79 | baseLayerLuminance.withDefault(StandardLuminance.DarkMode); 80 | switchHighlightStyle(true); 81 | } else /* Light */ { 82 | baseLayerLuminance.withDefault(StandardLuminance.LightMode); 83 | switchHighlightStyle(false); 84 | } 85 | } 86 | 87 | setInitialBaseLayerLuminance(); --------------------------------------------------------------------------------