diff --git a/azure-pipeline - Search.Algolia.yml b/azure-pipeline - Search.Algolia.yml index d75dbf7b..8236f7ff 100644 --- a/azure-pipeline - Search.Algolia.yml +++ b/azure-pipeline - Search.Algolia.yml @@ -14,15 +14,19 @@ steps: - task: NuGetToolInstaller@1 displayName: 'Install NuGet' -- task: NuGetCommand@2 +- task: DotNetCoreCLI@2 displayName: 'NuGet Restore' inputs: - restoreSolution: '$(solution)' + command: 'restore' + feedsToUse: 'select' + projects: '$(project)' + includeNuGetOrg: true - task: VSBuild@1 + displayName: 'Build Project' inputs: - solution: '$(solution)' - msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + solution: '$(project)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/AlgoliaComposer.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/AlgoliaComposer.cs new file mode 100644 index 00000000..a4cf005b --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/AlgoliaComposer.cs @@ -0,0 +1,40 @@ +using Algolia.Search.Models.Search; + +using Microsoft.Extensions.DependencyInjection; + +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Integrations.Search.Algolia; +using Umbraco.Cms.Integrations.Search.Algolia.Configuration; +using Umbraco.Cms.Integrations.Search.Algolia.Handlers; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; +using Umbraco.Cms.Integrations.Search.Algolia.Models; +using Umbraco.Cms.Integrations.Search.Algolia.Services; + +namespace Umbraco.Cms.Integrations.Crm.ActiveCampaign +{ + public class AlgoliaComposer : IComposer + { + public void Compose(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + + builder.AddNotificationAsyncHandler(); + + builder.AddNotificationAsyncHandler(); + + builder.AddNotificationAsyncHandler(); + + var options = builder.Services.AddOptions() + .Bind(builder.Config.GetSection(Constants.SettingsPath)); + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton>, AlgoliaSearchService>(); + + builder.Services.AddScoped, AlgoliaIndexDefinitionStorage>(); + } + + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/AlgoliaDashboard.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/AlgoliaDashboard.cs new file mode 100644 index 00000000..40a69c79 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/AlgoliaDashboard.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Dashboards; + +namespace Umbraco.Cms.Integrations.Search.Algolia +{ + [Weight(100)] + public class AlgoliaDashboard : IDashboard + { + public string[] Sections => new[] { Umbraco.Cms.Core.Constants.Applications.Settings }; + + public IAccessRule[] AccessRules => Array.Empty(); + + public string Alias => "algoliaSearchManagement"; + + public string View => "/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/dashboard.html"; + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/config/lang/en-US.xml b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/config/lang/en-US.xml new file mode 100644 index 00000000..bb705001 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/config/lang/en-US.xml @@ -0,0 +1,9 @@ + + + + Algolia Search Management + + + Push Data + + diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/css/algolia.css b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/css/algolia.css new file mode 100644 index 00000000..c88afe9b --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/css/algolia.css @@ -0,0 +1,5 @@ +.umb-content-grid { + display:grid; + grid-template-columns: repeat(3, 1fr); + gap:10px; +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.resource.js b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.resource.js new file mode 100644 index 00000000..c3cde6ee --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.resource.js @@ -0,0 +1,34 @@ +angular.module('umbraco.resources').factory('umbracoCmsIntegrationsSearchAlgoliaResource', + function ($http, umbRequestHelper) { + + const apiEndpoint = "backoffice/UmbracoCmsIntegrationsSearchAlgolia/Search"; + + return { + getIndices: function () { + return umbRequestHelper.resourcePromise( + $http.get(`${apiEndpoint}/GetIndices`), + "Failed"); + }, + saveIndex: function (id, name, contentData) { + return umbRequestHelper.resourcePromise( + $http.post(`${apiEndpoint}/SaveIndex`, { id: id, name: name, contentData: contentData }), + "Failed"); + }, + buildIndex: function (id) { + return umbRequestHelper.resourcePromise( + $http.post(`${apiEndpoint}/BuildIndex`, { id: id }), + "Failed"); + }, + deleteIndex: function (id) { + return umbRequestHelper.resourcePromise( + $http.delete(`${apiEndpoint}/DeleteIndex?id=${id}`), + "Failed"); + }, + search: function (indexId, query) { + return umbRequestHelper.resourcePromise( + $http.get(`${apiEndpoint}/Search?indexId=${indexId}&query=${query}`), + "Failed"); + } + }; + } +); \ No newline at end of file diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.service.js b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.service.js new file mode 100644 index 00000000..60fb780e --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.service.js @@ -0,0 +1,41 @@ +function algoliaService(contentTypeResource) { + return { + getContentTypes: function (callback) { + contentTypeResource.getAll().then(function (data) { + callback(data.filter(item => item.parentId == -1 && !item.isElement).map((item) => { + return { + id: item.id, + icon: item.icon, + alias: item.alias, + name: item.name, + selected: false, + allowRemove: false + } + })); + }); + }, + getPropertiesByContentTypeId: function (contentTypeId, callback) { + contentTypeResource.getById(contentTypeId).then(function (data) { + var properties = []; + + for (var i = 0; i < data.groups.length; i++) { + for (var j = 0; j < data.groups[i].properties.length; j++) { + properties.push({ + id: data.groups[i].properties[j].id, + icon: "icon-indent", + alias: data.groups[i].properties[j].alias, + name: data.groups[i].properties[j].label, + group: data.groups[i].name, + selected: false + }); + } + } + + callback(properties); + }); + } + } +} + +angular.module("umbraco.services") + .factory("algoliaService", algoliaService); \ No newline at end of file diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/dashboard.controller.js b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/dashboard.controller.js new file mode 100644 index 00000000..d3b1b71d --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/dashboard.controller.js @@ -0,0 +1,287 @@ +function dashboardController(notificationsService, overlayService, eventsService, algoliaService, umbracoCmsIntegrationsSearchAlgoliaResource) { + var vm = this; + + vm.loading = false; + + vm.searchQuery = ""; + vm.selectedSearchIndex = {}; + vm.searchResults = {}; + + vm.viewState = "list"; + + init(); + + vm.addIndex = addIndex; + vm.saveIndex = saveIndex; + vm.viewIndex = viewIndex; + vm.buildIndex = buildIndex; + vm.searchIndex = searchIndex; + vm.deleteIndex = deleteIndex; + vm.search = search; + + function init() { + /* contentData property: + [ + { + "contentType": { + "alias": "", + "name": "", + "icon": "" + }, + "properties": [ + { + "alias": "", + "name": "" + } + ] + } + ] + */ + vm.manageIndex = { + id: 0, + name: "", + selectedContentType: {}, + contentTypesList: [], + propertiesList: [], + includeProperties: [ + { + "alias": "alias", + "header": "Alias" + }, + { + "alias": "group", + "header": "Group" + } + ], + contentData: [], + showProperties: function (contentType) { + + this.selectedContentType = contentType; + + algoliaService.getPropertiesByContentTypeId(contentType.id, (response) => { + this.propertiesList = response; + + var contentTypeData = this.contentData.find(obj => obj.contentType.alias == contentType.alias); + if (contentTypeData && contentTypeData.properties.length > 0) { + vm.manageIndex.propertiesList = vm.manageIndex.propertiesList.map((obj) => { + if (contentTypeData.properties.find(p => p.alias == obj.alias)) { + obj.selected = true; + } + + return obj; + }); + } + }); + }, + removeContentType: function (contentType) { + + const contentTypeIndex = this.contentData.map(obj => obj.contentType.alias).indexOf(contentType.alias); + this.contentData.splice(contentTypeIndex, 1); + + this.selectedContentType = {}; + this.contentTypesList.forEach(obj => { + if (obj.alias == contentType.alias) { + obj.selected = false; + obj.allowRemove = false; + } + }); + this.propertiesList = []; + }, + selectProperty: function (property) { + + var contentDataItem = vm.manageIndex.contentData.find(obj => obj.contentType.alias == vm.manageIndex.selectedContentType.alias); + + var selected = !property.selected; + if (selected) { + // mark item selected + vm.manageIndex.propertiesList.find(obj => obj.alias == property.alias).selected = true; + + // check if content type exists in the contentData array + if (!contentDataItem) { + var contentItem = { + contentType: { + alias: vm.manageIndex.selectedContentType.alias, + name: vm.manageIndex.selectedContentType.name, + icon: vm.manageIndex.selectedContentType.icon + }, + properties: [] + }; + vm.manageIndex.contentData.push(contentItem); + + // select content type + vm.manageIndex.contentTypesList.forEach(obj => { + if (obj.alias == vm.manageIndex.selectedContentType.alias) { + obj.selected = true; + obj.allowRemove = true; + } + }); + } + + // add property + vm.manageIndex.contentData + .find(obj => obj.contentType.alias == vm.manageIndex.selectedContentType.alias) + .properties.push({ + alias: property.alias, + name: property.name + }); + } + else { + // deselect item + vm.manageIndex.propertiesList.find(obj => obj.alias == property.alias).selected = false; + + // remove property item + const propertyIndex = vm.manageIndex.contentData + .find(obj => obj.contentType.alias == vm.manageIndex.selectedContentType.alias) + .properties.map(obj => obj.alias).indexOf(property.alias); + vm.manageIndex.contentData + .find(obj => obj.contentType.alias == vm.manageIndex.selectedContentType.alias).properties.splice(propertyIndex, 1); + + // remove content type item with no properties and deselect + if (vm.manageIndex.contentData.find(obj => obj.contentType.alias == vm.manageIndex.selectedContentType.alias).properties.length == 0) { + vm.manageIndex.contentTypesList.find(obj => obj.alias == vm.manageIndex.selectedContentType.alias).selected = false; + vm.manageIndex.contentTypesList.find(obj => obj.alias == vm.manageIndex.selectedContentType.alias).allowRemove = false; + + const contentTypeIndex = vm.manageIndex.contentData.map(obj => obj.contentType.alias).indexOf(vm.manageIndex.selectedContentType.alias); + vm.manageIndex.contentData.splice(contentTypeIndex, 1); + } + } + }, + reset: function () { + this.visible = false; + this.id = 0; + this.name = ""; + this.selectedContentType = {}; + this.contentTypesList = []; + this.propertiesList = []; + this.contentData = []; + } + }; + + getIndices(); + } + + function getIndices() { + vm.indices = []; + + umbracoCmsIntegrationsSearchAlgoliaResource.getIndices().then(function (data) { + vm.indices = data; + }); + } + + function addIndex() { + vm.viewState = "manage"; + algoliaService.getContentTypes((response) => vm.manageIndex.contentTypesList = response); + } + + function saveIndex() { + + if (vm.manageIndex.name.length == 0 || vm.manageIndex.contentData.length == 0) { + notificationsService.error("Algolia", "Index name and content schema are required."); + return false; + } + + vm.loading = true; + + umbracoCmsIntegrationsSearchAlgoliaResource + .saveIndex(vm.manageIndex.id, vm.manageIndex.name, vm.manageIndex.contentData) + .then(function (response) { + if (response.success) { + vm.manageIndex.reset(); + algoliaService.getContentTypes((response) => vm.manageIndex.contentTypes = response); + notificationsService.success("Algolia", "Index saved."); + } else { + notificationsService.error("Algolia", response.error); + } + + vm.viewState = "list"; + + getIndices(); + + vm.loading = false; + }); + } + + function viewIndex(index) { + + vm.viewState = "manage"; + + vm.manageIndex.id = index.id; + vm.manageIndex.name = index.name; + vm.manageIndex.contentData = index.contentData; + + algoliaService.getContentTypes((response) => { + + vm.manageIndex.contentTypesList = response; + + for (var i = 0; i < vm.manageIndex.contentData.length; i++) { + + vm.manageIndex.contentTypesList.forEach(obj => { + if (obj.alias == vm.manageIndex.contentData[i].contentType.alias) { + obj.selected = true; + obj.allowRemove = true; + } + }); + } + }); + } + + function buildIndex(index) { + const dialogOptions = { + view: "/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/index.build.html", + index: index, + submit: function (model) { + vm.loading = true; + + umbracoCmsIntegrationsSearchAlgoliaResource.buildIndex(model.index.id).then(function (response) { + if (response.failure) + notificationsService.warning("Algolia", "An error has occurred while building the index: " + response.error); + else { + notificationsService.success("Algolia", "Index built successfully."); + vm.loading = false; + overlayService.close(); + } + }); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(dialogOptions); + } + + function searchIndex(index) { + vm.viewState = "search"; + vm.selectedSearchIndex = index; + } + + function deleteIndex(index) { + const dialogOptions = { + title: "Delete", + content: "Are you sure you want to delete index " + index.name + "?", + confirmType: "delete", + submit: function () { + umbracoCmsIntegrationsSearchAlgoliaResource.deleteIndex(index.id).then(function (response) { + if (response.success) { + notificationsService.success("Algolia", "Index deleted."); + getIndices(); + } else + notificationsService.error("Algolia", response.error); + + overlayService.close(); + }); + } + }; + + overlayService.confirm(dialogOptions); + } + + function search() { + umbracoCmsIntegrationsSearchAlgoliaResource.search(vm.selectedSearchIndex.id, vm.searchQuery).then(function (response) { + vm.searchResults = response; + }); + } +} + +angular.module("umbraco") + .controller("Umbraco.Cms.Integrations.Search.Algolia.DashboardController", dashboardController); \ No newline at end of file diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/package.manifest b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/package.manifest new file mode 100644 index 00000000..6cc9bd85 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/package.manifest @@ -0,0 +1,13 @@ +{ + "name": "Umbraco.Cms.Integrations.Search.Algolia", + "version": "1.0.0", + "allowPackageTelemetry": true, + "javascript": [ + "~/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.service.js", + "~/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/dashboard.controller.js", + "~/App_Plugins/UmbracoCms.Integrations/Search/Algolia/js/algolia.resource.js" + ], + "css": [ + "~/App_Plugins/UmbracoCms.Integrations/Search/Algolia/css/algolia.css" + ] +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/dashboard.html b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/dashboard.html new file mode 100644 index 00000000..fb4ce875 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/dashboard.html @@ -0,0 +1,231 @@ +
+ +
+ +
+ +
+ +
+
+
+ Algolia Indices +
+
+
+
+
Manage Algolia Indices
+
+

+ Algolia is an AI-powered search and discovery platform allowing you to create cutting-edge customer experiences for their websites or mobile apps. + It's like the perfect mediator between your website and customers, making sure the conversation is as smooth and efficient as possible. +

+

+ The Algolia model provides Search as a Service through an externally hosted search engine, offering web search across the website based + on the content payload pushed from the website to Algolia. +

+

+ To get started, you need to create an index and define the content schema - document types and properties. + Then you can build your index, push data to Algolia and run searches across created indices. +
+ + Read more about integrating Algolia Search + +

+
+
+ + +
+
+
+ + + + + + + + + + + + + + + +
NameSchema
+ + + + + + + + + + + +
+
+
+
+
+
+ + + + + +
+
+
+ {{ vm.manageIndex.id == 0 ? "Create" : "Edit" }} Index Definition +
+
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
Document Types
+
+ Please select the document types you would like to index,
and click Open to choose the fields to include. +
+ + +
+
+
{{ vm.manageIndex.selectedContentType.name }} Properties
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+ + + + + +
+
+
+ Search +
+
+
+ +
+
+
Search over index
+
+ Please enter the query you want to search by against index {{ vm.selectedSearchIndex.name }} +
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+ +
+

Items Count: {{ vm.searchResults.itemsCount }}

+

Pages Count: {{ vm.searchResults.pagesCount }}

+

Items per Page: {{ vm.searchResults.itemsPerPage }}

+
+

+ {{ key }} : {{ value }} +

+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/index.build.html b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/index.build.html new file mode 100644 index 00000000..5d08a85c --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/App_Plugins/UmbracoCms.Integrations/Search/Algolia/views/index.build.html @@ -0,0 +1,8 @@ +
+
+ This will cause the index to be built.
+ Depending on how much content there is in your site this could take a while.
+ It is not recommended to rebuild an index during times of high website traffic + or when editors are editing content. +
+
diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Builders/RecordBuilder.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Builders/RecordBuilder.cs new file mode 100644 index 00000000..ae5898f8 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Builders/RecordBuilder.cs @@ -0,0 +1,39 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Integrations.Search.Algolia.Models; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Builders +{ + public class RecordBuilder + { + private readonly Record _record = new(); + + public RecordBuilder BuildFromContent(IContent content, Func filter = null) + { + _record.ObjectID = content.Key.ToString(); + + foreach (var property in content.Properties.Where(filter ?? (p => true))) + { + if (!_record.Data.ContainsKey(property.Alias)) + _record.Data.Add(property.Alias, property.GetValue().ToString()); + } + + return this; + } + + public RecordBuilder BuildFromContent(IPublishedContent publishedContent, Func filter = null) + { + _record.ObjectID = publishedContent.Key.ToString(); + + foreach (var property in publishedContent.Properties.Where(filter ?? (p => true))) + { + if (!_record.Data.ContainsKey(property.Alias) && property.HasValue()) + _record.Data.Add(property.Alias, property.GetValue().ToString()); + } + + return this; + } + + public Record Build() => _record; + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Configuration/AlgoliaSettings.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Configuration/AlgoliaSettings.cs new file mode 100644 index 00000000..ff32739b --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Configuration/AlgoliaSettings.cs @@ -0,0 +1,12 @@ + +namespace Umbraco.Cms.Integrations.Search.Algolia.Configuration +{ + public class AlgoliaSettings + { + public string ApplicationId { get; set; } + + public string AdminApiKey { get; set; } + + public string ApiKey { get; set; } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Constants.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Constants.cs new file mode 100644 index 00000000..7044e2a2 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Constants.cs @@ -0,0 +1,10 @@ + +namespace Umbraco.Cms.Integrations.Search.Algolia +{ + public class Constants + { + public const string SettingsPath = "Umbraco:Cms:Integrations:Search:Algolia:Settings"; + + public const string AlgoliaIndicesTableName = "algoliaIndices"; + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Controllers/SearchController.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Controllers/SearchController.cs new file mode 100644 index 00000000..070e74bc --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Controllers/SearchController.cs @@ -0,0 +1,149 @@ +using Algolia.Search.Models.Search; + +using Microsoft.AspNetCore.Mvc; + +using System.Text.Json; +using Umbraco.Cms.Integrations.Search.Algolia.Builders; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; +using Umbraco.Cms.Integrations.Search.Algolia.Models; +using Umbraco.Cms.Integrations.Search.Algolia.Services; +using Umbraco.Cms.Web.BackOffice.Controllers; +using Umbraco.Cms.Web.Common; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Controllers +{ + [PluginController("UmbracoCmsIntegrationsSearchAlgolia")] + public class SearchController : UmbracoAuthorizedApiController + { + private readonly IAlgoliaIndexService _indexService; + + private readonly IAlgoliaSearchService> _searchService; + + private readonly IAlgoliaIndexDefinitionStorage _indexStorage; + + private readonly UmbracoHelper _umbracoHelper; + + public SearchController(IAlgoliaIndexService indexService, IAlgoliaSearchService> searchService, + IAlgoliaIndexDefinitionStorage indexStorage, + UmbracoHelper umbracoHelper) + { + _indexService = indexService; + + _searchService = searchService; + + _indexStorage = indexStorage; + + _umbracoHelper = umbracoHelper; + } + + [HttpGet] + public IActionResult GetIndices() + { + var results = _indexStorage.Get().Select(p => new IndexConfiguration + { + Id = p.Id, + Name = p.Name, + ContentData = JsonSerializer.Deserialize>(p.SerializedData) + }); + + return new JsonResult(results); + } + + [HttpPost] + public async Task SaveIndex([FromBody] IndexConfiguration index) + { + try + { + _indexStorage.AddOrUpdate(new AlgoliaIndex + { + Id = index.Id, + Name = index.Name, + SerializedData = JsonSerializer.Serialize(index.ContentData), + Date = DateTime.Now + }); + + var result = await _indexService.PushData(index.Name); + + return new JsonResult(result); + } + catch(Exception ex) + { + return new JsonResult(Result.Fail(ex.Message)); + } + } + + [HttpPost] + public async Task BuildIndex([FromBody] IndexConfiguration indexConfiguration) + { + try + { + var index = _indexStorage.GetById(indexConfiguration.Id); + + var payload = new List(); + + var indexContentData = JsonSerializer.Deserialize>(index.SerializedData); + + foreach (var contentDataItem in indexContentData) + { + var contentItems = _umbracoHelper.ContentAtXPath($"//{contentDataItem.ContentType}"); + + foreach (var contentItem in contentItems) + { + var record = new RecordBuilder() + .BuildFromContent(contentItem, (p) => contentDataItem.Properties.Any(q => q.Alias == p.Alias)) + .Build(); + + payload.Add(record); + } + } + + var result = await _indexService.PushData(index.Name, payload); + + return new JsonResult(result); + } + catch (Exception ex) + { + return new JsonResult(Result.Fail(ex.Message)); + } + } + + [HttpDelete] + public async Task DeleteIndex(int id) + { + try + { + var indexName = _indexStorage.GetById(id).Name; + + _indexStorage.Delete(id); + + await _indexService.DeleteIndex(indexName); + + return new JsonResult(Result.Ok()); + } + catch(Exception ex) + { + return new JsonResult(Result.Fail(ex.Message)); + } + } + + [HttpGet] + public IActionResult Search(int indexId, string query) + { + var index = _indexStorage.GetById(indexId); + + var searchResults = _searchService.Search(index.Name, query); + + var response = new Response + { + ItemsCount = searchResults.NbHits, + PagesCount = searchResults.NbPages, + ItemsPerPage = searchResults.HitsPerPage, + Hits = searchResults.Hits.Select(p => p.Data.ToDictionary(x => x.Key, y => y.Value.ToString())).ToList() + }; + + return new JsonResult(response); + } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/BaseContentHandler.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/BaseContentHandler.cs new file mode 100644 index 00000000..9aa6b626 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/BaseContentHandler.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using System.Text.Json; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Integrations.Search.Algolia.Builders; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; +using Umbraco.Cms.Integrations.Search.Algolia.Models; +using Umbraco.Cms.Integrations.Search.Algolia.Services; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Handlers +{ + public abstract class BaseContentHandler + { + protected readonly ILogger Logger; + + protected readonly IAlgoliaIndexDefinitionStorage IndexStorage; + + protected readonly IAlgoliaIndexService IndexService; + + public BaseContentHandler(ILogger logger, + IAlgoliaIndexDefinitionStorage indexStorage, + IAlgoliaIndexService indexService) + { + Logger = logger; + + IndexStorage = indexStorage; + + IndexService = indexService; + } + + protected async Task RebuildIndex(IEnumerable entities, bool deleteIndexData = false) + { + try + { + var indices = IndexStorage.Get(); + + foreach (var entity in entities) + { + foreach (var index in indices) + { + var indexConfiguration = JsonSerializer.Deserialize>(index.SerializedData) + .FirstOrDefault(p => p.ContentType.Alias == entity.ContentType.Alias); + if (indexConfiguration == null || indexConfiguration.ContentType.Alias != entity.ContentType.Alias) continue; + + var record = new RecordBuilder() + .BuildFromContent(entity, (p) => indexConfiguration.Properties.Any(q => q.Alias == p.Alias)) + .Build(); + + var result = deleteIndexData + ? await IndexService.DeleteData(index.Name, entity.Key.ToString()) + : await IndexService.UpdateData(index.Name, record); + + if (result.Failure) + Logger.LogError($"Failed to update data for Algolia index: {result}"); + } + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to update data for Algolia index: {ex.Message}"); + } + } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentDeletedHandler.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentDeletedHandler.cs new file mode 100644 index 00000000..b01b4a3f --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentDeletedHandler.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; +using Umbraco.Cms.Integrations.Search.Algolia.Services; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Handlers +{ + public class ContentDeletedHandler : BaseContentHandler, INotificationAsyncHandler + { + public ContentDeletedHandler(ILogger logger, IAlgoliaIndexDefinitionStorage indexStorage, IAlgoliaIndexService indexService) + : base(logger, indexStorage, indexService) + { } + + public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) => + await RebuildIndex(notification.DeletedEntities, true); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentPublishedHandler.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentPublishedHandler.cs new file mode 100644 index 00000000..4ca68c8b --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentPublishedHandler.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; +using Umbraco.Cms.Integrations.Search.Algolia.Services; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Handlers +{ + public class ContentPublishedHandler : BaseContentHandler, INotificationAsyncHandler + { + public ContentPublishedHandler(ILogger logger, IAlgoliaIndexDefinitionStorage indexStorage, IAlgoliaIndexService indexService) + : base(logger, indexStorage, indexService) + { } + + public async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) => await RebuildIndex(notification.PublishedEntities); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentUnpublishedHandler.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentUnpublishedHandler.cs new file mode 100644 index 00000000..aee6a4cf --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Handlers/ContentUnpublishedHandler.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +using System.Text.Json; + +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; +using Umbraco.Cms.Integrations.Search.Algolia.Models; +using Umbraco.Cms.Integrations.Search.Algolia.Services; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Handlers +{ + public class ContentUnpublishedHandler : BaseContentHandler, INotificationAsyncHandler + { + public ContentUnpublishedHandler(ILogger logger, IAlgoliaIndexDefinitionStorage indexStorage, IAlgoliaIndexService indexService) + : base(logger, indexStorage, indexService) + { } + + public async Task HandleAsync(ContentUnpublishedNotification notification, CancellationToken cancellationToken) => + await RebuildIndex(notification.UnpublishedEntities, true); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/AddAlgoliaIndicesTable.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/AddAlgoliaIndicesTable.cs new file mode 100644 index 00000000..6ae6c389 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/AddAlgoliaIndicesTable.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; + +using Umbraco.Cms.Infrastructure.Migrations; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Migrations +{ + public class AddAlgoliaIndicesTable : MigrationBase + { + public AddAlgoliaIndicesTable(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + Logger.LogDebug("Running migration {MigrationStep}", nameof(AddAlgoliaIndicesTable)); + + if (TableExists(Constants.AlgoliaIndicesTableName)) + Logger.LogDebug("The database table {DbTable} already exists, skipping.", Constants.AlgoliaIndicesTableName); + else + Create.Table().Do(); + } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/AlgoliaIndex.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/AlgoliaIndex.cs new file mode 100644 index 00000000..96b97fe0 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/AlgoliaIndex.cs @@ -0,0 +1,26 @@ +using NPoco; + +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Migrations +{ + [TableName(Constants.AlgoliaIndicesTableName)] + [PrimaryKey("Id", AutoIncrement = true)] + [ExplicitColumns] + public class AlgoliaIndex + { + [PrimaryKeyColumn(AutoIncrement = true, IdentitySeed = 1)] + [Column("Id")] + public int Id { get; set; } + + [Column("Name")] + public string Name { get; set; } + + [Column("SerializedData")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + public string SerializedData { get; set; } + + [Column("Date")] + public DateTime Date { get; set; } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/RunAlgoliaIndicesMigration.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/RunAlgoliaIndicesMigration.cs new file mode 100644 index 00000000..f2521399 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Migrations/RunAlgoliaIndicesMigration.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Migrations +{ + public class RunAlgoliaIndicesMigration : INotificationHandler + { + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly IKeyValueService _keyValueService; + private readonly IRuntimeState _runtimeState; + + public RunAlgoliaIndicesMigration( + ICoreScopeProvider coreScopeProvider, + IMigrationPlanExecutor migrationPlanExecutor, + IKeyValueService keyValueService, + IRuntimeState runtimeState) + { + _coreScopeProvider = coreScopeProvider; + + _migrationPlanExecutor = migrationPlanExecutor; + + _keyValueService = keyValueService; + + _runtimeState = runtimeState; + } + + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (_runtimeState.Level < Core.RuntimeLevel.Run) return; + + var migrationPlan = new MigrationPlan("AlgoliaIndices"); + + migrationPlan.From(string.Empty) + .To("algoliaindices-db"); + + var upgrader = new Upgrader(migrationPlan); + upgrader.Execute(_migrationPlanExecutor, _coreScopeProvider, _keyValueService); + } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Models/ContentData.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/ContentData.cs new file mode 100644 index 00000000..4c4d1690 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/ContentData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Models +{ + public class ContentData + { + [JsonPropertyName("contentType")] + public ContentEntity ContentType { get; set; } + + [JsonPropertyName("properties")] + public IEnumerable Properties { get; set; } + + [JsonPropertyName("propertiesDescription")] + public IEnumerable PropertiesDescription => Properties.Select(p => p.Name); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Models/ContentEntity.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/ContentEntity.cs new file mode 100644 index 00000000..cbe5c303 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/ContentEntity.cs @@ -0,0 +1,17 @@ + +using System.Text.Json.Serialization; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Models +{ + public class ContentEntity + { + [JsonPropertyName("alias")] + public string Alias { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("icon")] + public string Icon { get; set; } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Models/IndexConfiguration.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/IndexConfiguration.cs new file mode 100644 index 00000000..edfd4f65 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/IndexConfiguration.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Integrations.Search.Algolia.Models +{ + public class IndexConfiguration + { + public int Id { get; set; } + + public string Name { get; set; } + + public List ContentData { get; set; } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Record.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Record.cs new file mode 100644 index 00000000..9de8b48e --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Record.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Models +{ + public class Record + { + public Record() + { + Data = new Dictionary(); + } + + public string ObjectID { get; set; } + + public Dictionary Data { get; set; } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Response.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Response.cs new file mode 100644 index 00000000..2769e69d --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Response.cs @@ -0,0 +1,14 @@ + +namespace Umbraco.Cms.Integrations.Search.Algolia.Models +{ + public class Response + { + public int ItemsCount { get; set; } + + public int PagesCount { get; set; } + + public int ItemsPerPage { get; set; } + + public List> Hits { get; set; } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Result.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Result.cs new file mode 100644 index 00000000..87f4ba2d --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Models/Result.cs @@ -0,0 +1,35 @@ + +using System.Text.Json.Serialization; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Models +{ + public class Result + { + public bool Success { get; set; } + + public string Error { get; set; } + + public bool Failure => !Success; + + protected Result(bool success, string error) + { + if (success && !string.IsNullOrEmpty(error)) + { + throw new ArgumentException("A succesful Result cannot have an error message.", error); + } + + if (!success && string.IsNullOrEmpty(error)) + { + throw new ArgumentException("A failure Result must have an error message.", error); + } + + Success = success; + Error = error; + } + + public static Result Ok() => new (true, string.Empty); + + public static Result Fail(string message) => new (false, message); + + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaIndexDefinitionStorage.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaIndexDefinitionStorage.cs new file mode 100644 index 00000000..59a808d7 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaIndexDefinitionStorage.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Integrations.Search.Algolia.Migrations; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Services +{ + public class AlgoliaIndexDefinitionStorage : IAlgoliaIndexDefinitionStorage + { + private readonly IScopeProvider _scopeProvider; + + public AlgoliaIndexDefinitionStorage(IScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; + } + + public void AddOrUpdate(AlgoliaIndex entity) + { + using var scope = _scopeProvider.CreateScope(); + + if (entity.Id == 0) + scope.Database.Insert(entity); + else + scope.Database.Update(entity); + + scope.Complete(); + } + + public List Get() + { + using var scope = _scopeProvider.CreateScope(); + + return scope.Database.Fetch(); + } + + + public AlgoliaIndex GetById(int id) + { + using var scope = _scopeProvider.CreateScope(); + + return scope.Database.Single("where Id = " + id); + } + + public void Delete(int id) + { + using var scope = _scopeProvider.CreateScope(); + + var entity = scope.Database.SingleById(id); + + scope.Database.Delete(entity); + + scope.Complete(); + } + + + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaIndexService.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaIndexService.cs new file mode 100644 index 00000000..a9618356 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaIndexService.cs @@ -0,0 +1,105 @@ +using Algolia.Search.Clients; +using Algolia.Search.Exceptions; + +using Microsoft.Extensions.Options; + +using Umbraco.Cms.Integrations.Search.Algolia.Configuration; +using Umbraco.Cms.Integrations.Search.Algolia.Models; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Services +{ + public class AlgoliaIndexService : IAlgoliaIndexService + { + private readonly AlgoliaSettings _settings; + + public AlgoliaIndexService(IOptions options) + { + _settings = options.Value; + } + + public async Task PushData(string name, List payload = null) + { + try + { + var client = new SearchClient(_settings.ApplicationId, _settings.AdminApiKey); + + var index = client.InitIndex(name); + + await index.SaveObjectsAsync(payload != null + ? payload + : new List { + new Record { + ObjectID = Guid.NewGuid().ToString(), + Data = new Dictionary()} + }, autoGenerateObjectId: false); + + if (payload == null) + await index.ClearObjectsAsync(); + + return Result.Ok(); + } + catch(AlgoliaException ex) + { + return Result.Fail(ex.Message); + } + } + + public async Task UpdateData(string name, Record record) + { + try + { + var client = new SearchClient(_settings.ApplicationId, _settings.AdminApiKey); + + var index = client.InitIndex(name); + + var obj = index.GetObjects(new[] { record.ObjectID }).FirstOrDefault(); + if (obj != null) + await index.PartialUpdateObjectAsync(record); + else + await index.SaveObjectAsync(record, autoGenerateObjectId: false); + + return Result.Ok(); + } + catch (AlgoliaException ex) + { + return Result.Fail(ex.Message); + } + } + + public async Task DeleteData(string name, string objectId) + { + try + { + var client = new SearchClient(_settings.ApplicationId, _settings.AdminApiKey); + + var index = client.InitIndex(name); + + await index.DeleteObjectAsync(objectId); + + return Result.Ok(); + } + catch (AlgoliaException ex) + { + return Result.Fail(ex.Message); + } + } + + public async Task DeleteIndex(string name) + { + try + { + var client = new SearchClient(_settings.ApplicationId, _settings.AdminApiKey); + + var index = client.InitIndex(name); + + await index.DeleteAsync(); + + return Result.Ok(); + } + catch (AlgoliaException ex) + { + return Result.Fail(ex.Message); + } + } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaSearchService.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaSearchService.cs new file mode 100644 index 00000000..0b3c3315 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/AlgoliaSearchService.cs @@ -0,0 +1,31 @@ +using Algolia.Search.Clients; +using Algolia.Search.Models.Search; + +using Microsoft.Extensions.Options; + +using Umbraco.Cms.Integrations.Search.Algolia.Configuration; +using Umbraco.Cms.Integrations.Search.Algolia.Models; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Services +{ + public class AlgoliaSearchService : IAlgoliaSearchService> + { + private readonly AlgoliaSettings _settings; + + public AlgoliaSearchService(IOptions options) + { + _settings = options.Value; + } + + public SearchResponse Search(string indexName, string query) + { + var client = new SearchClient(_settings.ApplicationId, _settings.AdminApiKey); + + var index = client.InitIndex(indexName); + + var results = index.Search(new Query(query)); + + return results; + } + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaIndexDefinitionStorage.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaIndexDefinitionStorage.cs new file mode 100644 index 00000000..65da599c --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaIndexDefinitionStorage.cs @@ -0,0 +1,15 @@ + +namespace Umbraco.Cms.Integrations.Search.Algolia.Services +{ + public interface IAlgoliaIndexDefinitionStorage + where T : class + { + List Get(); + + T GetById(int id); + + void AddOrUpdate(T entity); + + void Delete(int id); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaIndexService.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaIndexService.cs new file mode 100644 index 00000000..7b1f775b --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaIndexService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Integrations.Search.Algolia.Models; + +namespace Umbraco.Cms.Integrations.Search.Algolia.Services +{ + public interface IAlgoliaIndexService + { + Task PushData(string name, List payload = null); + + Task UpdateData(string name, Record record); + + Task DeleteData(string name, string objectId); + + Task DeleteIndex(string name); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaSearchService.cs b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaSearchService.cs new file mode 100644 index 00000000..56c5c300 --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Services/IAlgoliaSearchService.cs @@ -0,0 +1,8 @@ + +namespace Umbraco.Cms.Integrations.Search.Algolia.Services +{ + public interface IAlgoliaSearchService + { + T Search(string indexName, string query); + } +} diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/Umbraco.Cms.Integrations.Search.Algolia.csproj b/src/Umbraco.Cms.Integrations.Search.Algolia/Umbraco.Cms.Integrations.Search.Algolia.csproj new file mode 100644 index 00000000..7d9f947d --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/Umbraco.Cms.Integrations.Search.Algolia.csproj @@ -0,0 +1,59 @@ + + + + net6.0 + enable + Nullable + + + + Umbraco.Cms.Integrations.Search.Algolia + Umbraco CMS Integrations: Search - Algolia + An extension for Umbraco CMS providing a custom search engine integration with Algolia. + + https://github.com/umbraco/Umbraco.Cms.Integrations/tree/main/src/Umbraco.Cms.Integrations.Search.Algolia + https://github.com/umbraco/Umbraco.Cms.Integrations + 1.0.0 + Umbraco HQ + Umbraco + Umbraco;Umbraco-Marketplace + algolia.png + + + + + + + + + + + + true + App_Plugins\UmbracoCms.Integrations\Search\Algolia\ + + + True + buildTransitive + + + + + + true + Always + + + + + + true + \ + + + + + + + + diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/algolia.png b/src/Umbraco.Cms.Integrations.Search.Algolia/algolia.png new file mode 100644 index 00000000..3bbce791 Binary files /dev/null and b/src/Umbraco.Cms.Integrations.Search.Algolia/algolia.png differ diff --git a/src/Umbraco.Cms.Integrations.Search.Algolia/umbraco-marketplace.json b/src/Umbraco.Cms.Integrations.Search.Algolia/umbraco-marketplace.json new file mode 100644 index 00000000..6ab3ef9b --- /dev/null +++ b/src/Umbraco.Cms.Integrations.Search.Algolia/umbraco-marketplace.json @@ -0,0 +1,55 @@ +{ + "AuthorDetails": { + "Description": "Umbraco HQ", + "Url": "https://umbraco.com/", + "ImageUrl": "https://avatars.githubusercontent.com/u/1419552?s=200&v=4" + }, + "Category": "CMS Extensions", + "LicenseType": "Free", + "PackageType": "Integration", + "PackagesByAuthor": [ + "Umbraco.Cms.Integrations.Commerce.CommerceTools", + "Umbraco.Cms.Integrations.Commerce.Shopify", + "Umbraco.Cms.Integrations.SEO.Semrush", + "Umbraco.Cms.Integrations.SEO.GoogleSearchConsole.URLInspectionTool", + "Umbraco.Cms.Integrations.Crm.Hubspot", + "Umbraco.Cms.Integrations.Crm.Dynamics", + "Umbraco.Cms.Integrations.Crm.ActiveCampaign", + "Umbraco.Cms.Integrations.Automation.Zapier" + ], + "RelatedPackages": [ + { + "PackageId": "Umbraco.Cms.Integrations.Commerce.CommerceTools", + "Description": "A product and category picker that can be added as a property editor for content, with a value converter providing a strongly typed model for rendering." + }, + { + "PackageId": "Umbraco.Cms.Integrations.Commerce.Shopify", + "Description": "A products picker that can be added as a property editor for content, with a value converter providing a strongly typed model for rendering." + }, + { + "PackageId": "Umbraco.Cms.Integrations.SEO.Semrush", + "Description": "A search tool available as a content app, helping editors research and use appropriate keywords for their content, to help with website search engine optimisation." + }, + { + "PackageId": "Umbraco.Cms.Integrations.SEO.GoogleSearchConsole.URLInspectionTool", + "Description": "A tool allowing programmatic access to URL-level data for properties managed in Google Search Console and the indexed version of a URL." + }, + { + "PackageId": "Umbraco.Cms.Integrations.Crm.Hubspot", + "Description": "A form picker and rendering component for Hubspot forms." + }, + { + "PackageId": "Umbraco.Cms.Integrations.Crm.Dynamics", + "Description": "A form picker and rendering component for Dynamics 365 Marketing forms." + }, + { + "PackageId": "Umbraco.Cms.Integrations.Crm.ActiveCampaign", + "Description": "A form picker and rendering component for ActiveCampaign forms." + }, + { + "PackageId": "Umbraco.Cms.Integrations.Automation.Zapier", + "Description": "A dashboard interface allowing users to vizualize registered subscription hooks for content types and to call Zapier triggers when content gets published." + } + ], + "Tags": [] +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Integrations.sln b/src/Umbraco.Cms.Integrations.sln index 4564ec35..51fbf64b 100644 --- a/src/Umbraco.Cms.Integrations.sln +++ b/src/Umbraco.Cms.Integrations.sln @@ -61,7 +61,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Integrations.Te EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ActiveCampaign", "ActiveCampaign", "{1A4D3D38-F5B2-4528-92A1-318A7D09949D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Integrations.Crm.ActiveCampaign", "Umbraco.Cms.Integrations.Crm.ActiveCampaign\Umbraco.Cms.Integrations.Crm.ActiveCampaign.csproj", "{8FC3A87F-C10E-4605-9D24-BFF46D472170}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Integrations.Crm.ActiveCampaign", "Umbraco.Cms.Integrations.Crm.ActiveCampaign\Umbraco.Cms.Integrations.Crm.ActiveCampaign.csproj", "{8FC3A87F-C10E-4605-9D24-BFF46D472170}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Search", "Search", "{F56605AE-2258-4F61-B454-4247334DFC26}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Algolia", "Algolia", "{F2CAA1F7-9BED-4EB6-8875-D176B92D393A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Integrations.Search.Algolia", "Umbraco.Cms.Integrations.Search.Algolia\Umbraco.Cms.Integrations.Search.Algolia.csproj", "{54A624E5-5321-4CC8-B74B-11ABF3605242}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -121,6 +127,10 @@ Global {8FC3A87F-C10E-4605-9D24-BFF46D472170}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FC3A87F-C10E-4605-9D24-BFF46D472170}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FC3A87F-C10E-4605-9D24-BFF46D472170}.Release|Any CPU.Build.0 = Release|Any CPU + {54A624E5-5321-4CC8-B74B-11ABF3605242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54A624E5-5321-4CC8-B74B-11ABF3605242}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54A624E5-5321-4CC8-B74B-11ABF3605242}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54A624E5-5321-4CC8-B74B-11ABF3605242}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,6 +154,8 @@ Global {E837A4C4-8518-42AA-AC75-EA1CA0DE3EC4} = {8764ECBB-1B39-4D8A-A86B-ECD0D3F0F3D6} {1A4D3D38-F5B2-4528-92A1-318A7D09949D} = {4BDA951C-9C9D-4231-96CB-B1D9B75AF63B} {8FC3A87F-C10E-4605-9D24-BFF46D472170} = {1A4D3D38-F5B2-4528-92A1-318A7D09949D} + {F2CAA1F7-9BED-4EB6-8875-D176B92D393A} = {F56605AE-2258-4F61-B454-4247334DFC26} + {54A624E5-5321-4CC8-B74B-11ABF3605242} = {F2CAA1F7-9BED-4EB6-8875-D176B92D393A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2FB51E08-A3C8-4DFF-B3CB-E99C2ED021D5}