diff --git a/zero.Core/Database/Indexes/Backoffice_Search.cs b/zero.Core/Database/Indexes/Backoffice_Search.cs index c788a74f..0da6819c 100644 --- a/zero.Core/Database/Indexes/Backoffice_Search.cs +++ b/zero.Core/Database/Indexes/Backoffice_Search.cs @@ -1,78 +1,22 @@ -using Raven.Client.Documents.Indexes; -using System.Collections.Generic; -using System.Linq; -using zero.Core.Entities; +using Raven.Client.Documents; +using zero.Core.Options; namespace zero.Core.Database.Indexes { - public class Backoffice_Search : ZeroMultiMapIndex + public class Backoffice_Search : ZeroJavascriptIndex { - public class Result + public override void Setup(IZeroOptions options, IDocumentStore store) { - public string Id { get; set; } + // TODO index.Conventions is null, but needed for collection name retrieval - public string Group { get; set; } - - public string Name { get; set; } - - public bool IsActive { get; set; } - - public List Fields { get; set; } = new(); - } - - - protected override void Create() - { - AddEntities(); - Index(nameof(Result.Name), FieldIndexing.Search); - Index(nameof(Result.Fields), FieldIndexing.Search); - } - - - protected virtual void AddEntities() - { - AddPages(); - AddMediaFolders(); - //AddMedia(); - } - - - protected virtual void AddPages() - { - AddMap(map => map.Select(page => new Result() + foreach (var map in options.Search.GetAllItems()) { - Id = page.Id, - Group = "zero.page", - Name = page.Name, - IsActive = page.IsActive, - Fields = new() - })); - } + Maps.Add(map.BuildInstruction(this, store)); + } - protected virtual void AddMediaFolders() - { - AddMap(map => map.Select(page => new Result() - { - Id = page.Id, - Group = "zero.mediafolder", - Name = page.Name, - IsActive = page.IsActive, - Fields = new() - })); - } - - - protected virtual void AddMedia() - { - AddMap(map => map.Select(page => new Result() - { - Id = page.Id, - Group = "zero.media", - Name = page.Name, - IsActive = page.IsActive, - Fields = new() - })); + //Index(nameof(SearchIndexResult.Name), FieldIndexing.Search); + //Index(nameof(SearchIndexResult.Fields), FieldIndexing.Search); } } } diff --git a/zero.Core/Database/ZeroIndex.cs b/zero.Core/Database/ZeroIndex.cs index 91517dc6..2e1c32bd 100644 --- a/zero.Core/Database/ZeroIndex.cs +++ b/zero.Core/Database/ZeroIndex.cs @@ -1,4 +1,5 @@ -using Raven.Client.Documents.Indexes; +using Raven.Client.Documents; +using Raven.Client.Documents.Indexes; using Raven.Client.Documents.Indexes.Spatial; using Raven.Client.Documents.Operations.Attachments; using System; @@ -12,7 +13,9 @@ namespace zero.Core.Database public abstract class ZeroJavascriptIndex : AbstractJavaScriptIndexCreationTask, IZeroIndexDefinition { public ZeroJavascriptIndex() { Create(); } - protected abstract void Create(); + protected virtual void Create() { } + + public virtual void Setup(IZeroOptions options, IDocumentStore store) { } public new string Reduce { get => base.Reduce; set => base.Reduce = value; } public new string OutputReduceToCollection { get => base.OutputReduceToCollection; set => base.OutputReduceToCollection = value; } @@ -47,7 +50,9 @@ namespace zero.Core.Database public abstract class ZeroMultiMapIndex : AbstractMultiMapIndexCreationTask, IZeroIndexDefinition { public ZeroMultiMapIndex() { Create(); } - protected abstract void Create(); + protected virtual void Create() { } + + public virtual void Setup(IZeroOptions options, IDocumentStore store) { } // AbstractMultiMapIndexCreationTask public new void AddMap(Expression, IEnumerable>> map) => base.AddMap(map); @@ -112,7 +117,9 @@ namespace zero.Core.Database public abstract class ZeroMultiMapIndex : AbstractMultiMapIndexCreationTask, IZeroIndexDefinition { public ZeroMultiMapIndex() { Create(); } - protected abstract void Create(); + protected virtual void Create() { } + + public virtual void Setup(IZeroOptions options, IDocumentStore store) { } // AbstractMultiMapIndexCreationTask public new void AddMap(Expression, IEnumerable>> map) => base.AddMap(map); @@ -177,7 +184,9 @@ namespace zero.Core.Database public abstract class ZeroIndex : AbstractIndexCreationTask, IZeroIndexDefinition { public ZeroIndex() { Create(); } - protected abstract void Create(); + protected virtual void Create() { } + + public virtual void Setup(IZeroOptions options, IDocumentStore store) { } // AbstractIndexCreationTask public new Expression, IEnumerable>> Map { get => base.Map; set => base.Map = value; } @@ -246,7 +255,9 @@ namespace zero.Core.Database public abstract class ZeroIndex : AbstractIndexCreationTask, IZeroIndexDefinition { public ZeroIndex() { Create(); } - protected abstract void Create(); + protected virtual void Create() { } + + public virtual void Setup(IZeroOptions options, IDocumentStore store) { } // AbstractIndexCreationTask public new IJsonObject AsJson(object doc) => base.AsJson(doc); @@ -276,15 +287,6 @@ namespace zero.Core.Database public interface IZeroIndexDefinition : IAbstractIndexCreationTask { - void Setup(IZeroOptions options) - { - IEnumerable modifiers = options.Raven.Indexes.Modifiers.GetAllForType(this.GetType()); - - foreach (RavenIndexModifiersOptions.Modifier modifier in modifiers) - { - Action action = modifier.Modify.Compile(); - action.Invoke(this); - } - } + void Setup(IZeroOptions options, IDocumentStore store); } } diff --git a/zero.Core/Entities/SearchResult.cs b/zero.Core/Entities/SearchResult.cs new file mode 100644 index 00000000..2c72af00 --- /dev/null +++ b/zero.Core/Entities/SearchResult.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace zero.Core.Models +{ + public class SearchResult + { + public string Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public string Icon { get; set; } + + public string Image { get; set; } + + public string Url { get; set; } + + public string Group { get; set; } + + public bool IsActive { get; set; } + } + + + public class SearchIndexResult + { + public string Id { get; set; } + + public string Group { get; set; } + + public string Name { get; set; } + + public bool IsActive { get; set; } + + public List Fields { get; set; } = new(); + } +} diff --git a/zero.Core/Extensions/ZeroIndexExtensions.cs b/zero.Core/Extensions/ZeroIndexExtensions.cs new file mode 100644 index 00000000..ccbe5c76 --- /dev/null +++ b/zero.Core/Extensions/ZeroIndexExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using zero.Core.Database; +using zero.Core.Options; + +namespace zero.Core.Extensions +{ + public static class ZeroIndexExtensions + { + internal static void RunModifiers(this T index, IZeroOptions options) where T : IZeroIndexDefinition + { + IEnumerable modifiers = options.Raven.Indexes.Modifiers.GetAllForType(index.GetType()); + + foreach (RavenIndexModifiersOptions.Modifier modifier in modifiers) + { + Action action = modifier.Modify.Compile(); + action.Invoke(index); + } + } + } +} diff --git a/zero.Core/Options/RavenOptions.cs b/zero.Core/Options/RavenOptions.cs index 53c70b1e..2cfe299f 100644 --- a/zero.Core/Options/RavenOptions.cs +++ b/zero.Core/Options/RavenOptions.cs @@ -1,9 +1,11 @@ -using Raven.Client.Documents.Indexes; +using Raven.Client.Documents; +using Raven.Client.Documents.Indexes; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using zero.Core.Database; +using zero.Core.Extensions; namespace zero.Core.Options { @@ -19,45 +21,71 @@ namespace zero.Core.Options } - public class RavenIndexesOptions : ZeroBackofficeCollection, IZeroCollectionOptions + public class RavenIndexesOptions : ZeroBackofficeCollection, IZeroCollectionOptions { + public class Map + { + internal Type Type { get; set; } + + internal Expression> CreateIndex { get; set; } + + internal Map(Type type, Expression> create) + { + Type = type; + CreateIndex = create; + } + } + + public RavenIndexModifiersOptions Modifiers { get; private set; } = new(); public void Add() where T : IZeroIndexDefinition, new() { - Items.Add(typeof(T)); + Items.Add(new(typeof(T), () => new T())); } public void Add(Type indexType) { - Items.Add(indexType); + Items.Add(new(indexType, () => (IZeroIndexDefinition)Activator.CreateInstance(indexType))); + } + + public void Add(T index) where T : IZeroIndexDefinition + { + Items.Add(new(typeof(T), () => index)); } public void AddRange(params Type[] indexes) { - Items.AddRange(indexes); + foreach (Type type in indexes) + { + Add(type); + } } public void Replace() where T : IZeroIndexDefinition, new() where TReplaceWith : IZeroIndexDefinition, new() { - Items.Remove(typeof(T)); - Items.Add(typeof(TReplaceWith)); + Replace(typeof(T), typeof(TReplaceWith)); } public void Replace(Type origin, Type replaceWith) { - Items.Remove(origin); - Items.Add(replaceWith); + var item = Items.FirstOrDefault(x => x.Type == origin); + if (item != null) + { + Items.Remove(item); + } + Add(replaceWith); } - public IEnumerable GetAllForRegistration(IZeroOptions options) + public IEnumerable BuildAll(IZeroOptions options, IDocumentStore store) { - foreach (Type type in Items) + foreach (Map map in Items) { - IZeroIndexDefinition index = (IZeroIndexDefinition)Activator.CreateInstance(type); - index.Setup(options); + IZeroIndexDefinition index = map.CreateIndex.Compile().Invoke(); + index.Setup(options, store); + index.RunModifiers(options); yield return index; } } diff --git a/zero.Core/Options/SearchOptions.cs b/zero.Core/Options/SearchOptions.cs new file mode 100644 index 00000000..67cbb955 --- /dev/null +++ b/zero.Core/Options/SearchOptions.cs @@ -0,0 +1,106 @@ +using Raven.Client.Documents; +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using zero.Core.Database; +using zero.Core.Entities; +using zero.Core.Models; +using zero.Core.Renderer; + +namespace zero.Core.Options +{ + public class SearchOptions : ZeroBackofficeCollection, IZeroCollectionOptions + { + public bool IsEnabled { get; set; } + + + public SearchOptions() + { + IsEnabled = true; + Map(); + Map(); + } + + + public SearchIndexMap Map(string group = null) where T : ZeroEntity, new() + { + SearchIndexMap map = new(group); + Items.Add(map); + return map; + } + } + + + public class SearchIndexMap + { + protected Type _type; + protected string _group; + protected string[] _fields; + protected Func _modify; + + const string mapTemplate = @"map('{collection}', function (x) { + return { + Id: x.Id, + Group: '{group}', + Name: x.Name, + IsActive: x.IsActive, + Fields: [{fields}] + }; + });"; + + internal SearchIndexMap(Type type, string group) + { + _type = type; + _group = group; + } + + internal string BuildInstruction(IZeroIndexDefinition index, IDocumentStore store) + { + return TokenReplacement.Apply(mapTemplate, new() + { + { "collection", store.Conventions.GetCollectionName(_type) }, + { "group", _group }, + { "fields", BuildFieldArray(_fields) } + }); + } + + internal string BuildFieldArray(string[] fields) + { + if (fields == null || !fields.Any()) + { + return String.Empty; + } + + return "x." + String.Join(", x.", fields); + } + } + + + public class SearchIndexMap : SearchIndexMap where T : ZeroEntity + { + internal SearchIndexMap(string group) : base(typeof(T), group) {} + + public SearchIndexMap Display(Action modify = null) + { + _modify = (x, res) => + { + modify?.Invoke(x as T, res); + return Task.CompletedTask; + }; + return this; + } + + public SearchIndexMap Display(Func modify = null) + { + _modify = (x, res) => modify?.Invoke(x as T, res); + return this; + } + + public SearchIndexMap Fields(params string[] fieldNames) + { + _fields = fieldNames; + return this; + } + } +} diff --git a/zero.Core/Options/ZeroOptions.cs b/zero.Core/Options/ZeroOptions.cs index 76c47e6f..885bd31a 100644 --- a/zero.Core/Options/ZeroOptions.cs +++ b/zero.Core/Options/ZeroOptions.cs @@ -40,6 +40,8 @@ namespace zero.Core.Options Raven.Indexes.Add(); Raven.Indexes.Add(); Raven.Indexes.Add(); + + Search = new(); } /// @@ -106,6 +108,9 @@ namespace zero.Core.Options /// public InterceptorOptions Interceptors { get; private set; } + + /// + public SearchOptions Search { get; private set; } } @@ -211,5 +216,10 @@ namespace zero.Core.Options /// /// InterceptorOptions Interceptors { get; } + + /// + /// + /// + SearchOptions Search { get; } } } diff --git a/zero.Web.UI/App/search.vue b/zero.Web.UI/App/search.vue index 448d5ecd..fd2346fa 100644 --- a/zero.Web.UI/App/search.vue +++ b/zero.Web.UI/App/search.vue @@ -28,7 +28,7 @@ name: 'app-search', data: () => ({ - open: false, + open: true, query: null, model: { page: 1, diff --git a/zero.Web/Controllers/SearchController.cs b/zero.Web/Controllers/SearchController.cs index 1c1b8da9..ed729570 100644 --- a/zero.Web/Controllers/SearchController.cs +++ b/zero.Web/Controllers/SearchController.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using zero.Core.Entities; using zero.Core.Identity; +using zero.Core.Models; using zero.Web.Services; namespace zero.Web.Controllers @@ -16,6 +17,6 @@ namespace zero.Web.Controllers SearchService = searchService; } - public async Task> Query([FromQuery] string query) => await SearchService.Query(query); + public async Task> Query([FromQuery] string query) => await SearchService.Query(query); } } diff --git a/zero.Web/Services/BackofficeSearchService.cs b/zero.Web/Services/BackofficeSearchService.cs index 981e42a0..4564b6af 100644 --- a/zero.Web/Services/BackofficeSearchService.cs +++ b/zero.Web/Services/BackofficeSearchService.cs @@ -1,11 +1,15 @@ using Raven.Client.Documents; +using Raven.Client.Documents.Queries; using Raven.Client.Documents.Session; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using zero.Core.Database; using zero.Core.Database.Indexes; using zero.Core.Entities; using zero.Core.Extensions; +using zero.Core.Models; namespace zero.Web.Services { @@ -19,22 +23,40 @@ namespace zero.Web.Services } - public async Task> Query(string searchTerm) + public async Task> Query(string searchTerm) { - List results = await Session.Query() + string[] searchParts = searchTerm.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x => + { + return "*" + x + "*"; + }).ToArray(); + + List results = await Session.Query() .Statistics(out QueryStatistics stats) - .Search(x => x.Name, searchTerm, 2) - .Search(x => x.Fields, searchTerm, 1, SearchOptions.Or) + .Search(x => x.Name, searchParts, 2, @operator: SearchOperator.And) + .Search(x => x.Fields, searchParts, 1, SearchOptions.Or, @operator: SearchOperator.And) .Paging(1, 20) .ProjectInto() .ToListAsync(); - return new ListResult(results, stats.TotalResults, 1, 20); + List items = new(); + + foreach (ZeroEntity result in results) + { + items.Add(new() + { + Id = result.Id, + Name = result.Name, + IsActive = result.IsActive, + Url = "/" + }); + } + + return new ListResult(items, stats.TotalResults, 1, 20); } } public interface IBackofficeSearchService { - Task> Query(string searchTerm); + Task> Query(string searchTerm); } } diff --git a/zero.Web/ZeroBuilder.cs b/zero.Web/ZeroBuilder.cs index ff399103..3e0d594d 100644 --- a/zero.Web/ZeroBuilder.cs +++ b/zero.Web/ZeroBuilder.cs @@ -159,7 +159,7 @@ namespace zero.Web // create all indexes if (options.SetupCompleted) { - var indexes = options.Raven.Indexes.GetAllForRegistration(options); + var indexes = options.Raven.Indexes.BuildAll(options, store); IndexCreation.CreateIndexes(indexes, store, database: options.Raven.Database); }