diff --git a/.github/workflows/package_push_nuget.org.yml b/.github/workflows/package_push_nuget.org.yml index ee174457c..d10da2113 100644 --- a/.github/workflows/package_push_nuget.org.yml +++ b/.github/workflows/package_push_nuget.org.yml @@ -10,33 +10,51 @@ jobs: steps: - name: git pull uses: actions/checkout@v2 - + - name: run a one-line script run: env - - - name: setting dotnet version + + - name: setting dotnet version uses: actions/setup-dotnet@v1 with: dotnet-version: '6.0.x' include-prerelease: true - name: dependencies run: git clone -b main https://github.com/masastack/MASA.BuildingBlocks.git ./src/BuildingBlocks/MASA.BuildingBlocks - + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - name: Start Elasticsearch + uses: everpcpc/elasticsearch-action@v2 + with: + version: 7.6.1 + plugins: | + https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.6.1/elasticsearch-analysis-ik-7.6.1.zip + https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v7.6.1/elasticsearch-analysis-pinyin-7.6.1.zip + analysis-icu + analysis-smartcn + analysis-kuromoji + - name: restore run: dotnet restore - + - name: build run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true - + - name: test run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[*.Tests]*" - + - name: codecov uses: codecov/codecov-action@v1 - + - name: pack run: dotnet pack --include-symbols -p:PackageVersion=$GITHUB_REF_NAME - + - name: package push run: dotnet nuget push "**/*.symbols.nupkg" -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json - + diff --git a/Masa.Contrib.sln b/Masa.Contrib.sln index 09ba21992..90cd0517a 100644 --- a/Masa.Contrib.sln +++ b/Masa.Contrib.sln @@ -132,6 +132,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.BuildingBlocks.Service EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests", "test\Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests\Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests.csproj", "{4A052E17-4D9E-41EF-89A5-73B917053F8E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.SearchEngine.AutoComplete", "src\SearchEngine\Masa.Contrib.SearchEngine.AutoComplete\Masa.Contrib.SearchEngine.AutoComplete.csproj", "{3F8532EF-3DC9-45F8-9562-994ABE066585}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.SearchEngine.AutoComplete.Tests", "test\Masa.Contrib.SearchEngine.AutoComplete.Tests\Masa.Contrib.SearchEngine.AutoComplete.Tests.csproj", "{31262D61-26A4-4302-968D-52B8DA4558CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -484,6 +488,22 @@ Global {4A052E17-4D9E-41EF-89A5-73B917053F8E}.Release|Any CPU.Build.0 = Release|Any CPU {4A052E17-4D9E-41EF-89A5-73B917053F8E}.Release|x64.ActiveCfg = Release|Any CPU {4A052E17-4D9E-41EF-89A5-73B917053F8E}.Release|x64.Build.0 = Release|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Debug|x64.Build.0 = Debug|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Release|Any CPU.Build.0 = Release|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Release|x64.ActiveCfg = Release|Any CPU + {3F8532EF-3DC9-45F8-9562-994ABE066585}.Release|x64.Build.0 = Release|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Debug|x64.Build.0 = Debug|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|Any CPU.Build.0 = Release|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|x64.ActiveCfg = Release|Any CPU + {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -549,6 +569,8 @@ Global {2F4986D6-3F56-4C05-8A1D-399594F96093} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} {E72E105D-B15F-4D69-9A13-CAA49D4889D6} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} {4A052E17-4D9E-41EF-89A5-73B917053F8E} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} + {3F8532EF-3DC9-45F8-9562-994ABE066585} = {8C39C640-0E8A-43A7-890C-9742B6B70AA4} + {31262D61-26A4-4302-968D-52B8DA4558CD} = {38E6C400-90C0-493E-9266-C1602E229F1B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {40383055-CC50-4600-AD9A-53C14F620D03} diff --git a/src/BasicAbility/Masa.Contrib.BasicAbility.Dcc/Masa.Contrib.BasicAbility.Dcc.csproj b/src/BasicAbility/Masa.Contrib.BasicAbility.Dcc/Masa.Contrib.BasicAbility.Dcc.csproj index 0a38ee85b..e231b84ab 100644 --- a/src/BasicAbility/Masa.Contrib.BasicAbility.Dcc/Masa.Contrib.BasicAbility.Dcc.csproj +++ b/src/BasicAbility/Masa.Contrib.BasicAbility.Dcc/Masa.Contrib.BasicAbility.Dcc.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/BuildingBlocks/MASA.BuildingBlocks b/src/BuildingBlocks/MASA.BuildingBlocks index 21b03761f..70ccb4bd4 160000 --- a/src/BuildingBlocks/MASA.BuildingBlocks +++ b/src/BuildingBlocks/MASA.BuildingBlocks @@ -1 +1 @@ -Subproject commit 21b03761fdcc4549031cecbda1bfbfd41fa4df01 +Subproject commit 70ccb4bd42f6e9cf068e909278c2d8b6bcd776e3 diff --git a/src/Data/Masa.Contrib.Data.Contracts.EF/Masa.Contrib.Data.Contracts.EF.csproj b/src/Data/Masa.Contrib.Data.Contracts.EF/Masa.Contrib.Data.Contracts.EF.csproj index 4678288b7..19840adb9 100644 --- a/src/Data/Masa.Contrib.Data.Contracts.EF/Masa.Contrib.Data.Contracts.EF.csproj +++ b/src/Data/Masa.Contrib.Data.Contracts.EF/Masa.Contrib.Data.Contracts.EF.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Data/Masa.Contrib.Data.UoW.EF/Masa.Contrib.Data.UoW.EF.csproj b/src/Data/Masa.Contrib.Data.UoW.EF/Masa.Contrib.Data.UoW.EF.csproj index e5d3671d0..f9af68279 100644 --- a/src/Data/Masa.Contrib.Data.UoW.EF/Masa.Contrib.Data.UoW.EF.csproj +++ b/src/Data/Masa.Contrib.Data.UoW.EF/Masa.Contrib.Data.UoW.EF.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs b/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs index 3d321d5a7..3adc8766b 100644 --- a/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs +++ b/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs @@ -96,9 +96,8 @@ await executionStrategy.ExecuteAsync(strategyOptions, @event, async @event => }, (@event, ex, failureLevels) => { if (failureLevels != FailureLevels.Ignore) - { - throw ex; - } + throw new Exception(ex.Message, ex); + logger?.LogWarning("----- Publishing event {@Event} rollback error ignored: message id: {messageId} -----", @event, @event.Id); return Task.CompletedTask; }); diff --git a/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Masa.Contrib.Dispatcher.Events.csproj b/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Masa.Contrib.Dispatcher.Events.csproj index 0625240ef..133ba4d28 100644 --- a/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Masa.Contrib.Dispatcher.Events.csproj +++ b/src/Dispatcher/Masa.Contrib.Dispatcher.Events/Masa.Contrib.Dispatcher.Events.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj b/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj index f62ab4315..549eb9cee 100644 --- a/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj +++ b/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj b/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj index 6cba6252d..657310796 100644 --- a/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj +++ b/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/AutoCompleteClient.cs b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/AutoCompleteClient.cs new file mode 100644 index 000000000..42e94e5bd --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/AutoCompleteClient.cs @@ -0,0 +1,135 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete; + +public class AutoCompleteClient : IAutoCompleteClient +{ + private readonly IElasticClient _elasticClient; + private readonly IMasaElasticClient _client; + private readonly string _indexName; + private readonly Operator _defaultOperator; + private readonly SearchType _defaultSearchType; + + public AutoCompleteClient(IElasticClient elasticClient, IMasaElasticClient client, string indexName, Operator defaultOperator, SearchType defaultSearchType) + { + _elasticClient = elasticClient; + _client = client; + _indexName = indexName; + _defaultOperator = defaultOperator; + _defaultSearchType = defaultSearchType; + } + + public Task, TValue>> GetAsync(string value, + AutoCompleteOptions? options = null, + CancellationToken cancellationToken = default) + => GetAsync, TValue>(value, options, cancellationToken); + + public async Task> GetAsync(string value, + AutoCompleteOptions? options = null, + CancellationToken cancellationToken = default) + where TResponse : AutoCompleteDocument + { + var newOptions = options ?? new (_defaultSearchType); + if (newOptions.SearchType == SearchType.Fuzzy) + { + var ret = await _client.GetPaginatedListAsync( + new PaginatedOptions( + _indexName, + value, + newOptions.Field, + newOptions.Page, + newOptions.PageSize, + _defaultOperator) + , cancellationToken); + return new GetResponse(ret.IsValid, ret.Message) + { + Total = ret.Total, + TotalPages = ret.TotalPages, + Data = ret.Data + }; + } + else + { + var ret = await _elasticClient.SearchAsync(s => s + .Index(_indexName) + .From((newOptions.Page - 1) * newOptions.PageSize) + .Size(newOptions.PageSize) + .Query(q => q + .Bool(b => b + .Must(queryContainerDescriptor => queryContainerDescriptor.Term(newOptions.Field, value)))) + , cancellationToken + ); + return new GetResponse(ret.IsValid, ret.ServerError?.ToString() ?? "") + { + Data = ret.Hits.Select(hit => hit.Source).ToList(), + Total = ret.Total, + TotalPages = (int)Math.Ceiling(ret.Total / (decimal)newOptions.PageSize) + }; + } + } + + public Task SetAsync( + AutoCompleteDocument[] results, + SetOptions? options = null, + CancellationToken cancellationToken = default) + => SetAsync, TValue>(results, options, cancellationToken); + + public Task SetAsync( + TDocument[] documents, + SetOptions? options = null, + CancellationToken cancellationToken = default) where TDocument : AutoCompleteDocument + { + SetOptions newOptions = options ?? new(); + if (newOptions.IsOverride) + return SetAsync(documents, cancellationToken); + + return SetByNotOverrideAsync(documents, cancellationToken); + } + + /// + /// Set documents in batches + /// add them if they don’t exist, update them if they exist + /// + /// + /// + /// + /// + /// + private async Task SetAsync( + TDocument[] documents, + CancellationToken cancellationToken = default) + where TDocument : AutoCompleteDocument + { + var request = new SetDocumentRequest(_indexName); + foreach (var document in documents) + request.AddDocument(document, document.Id); + + var ret = await _client.SetDocumentAsync(request, cancellationToken); + return new SetResponse(ret.IsValid, ret.Message) + { + Items = ret.Items.Select(item => new SetResponseItems(item.Id, item.IsValid, item.Message)).ToList() + }; + } + + /// + /// Set documents in batches + /// Update if it does not exist, skip if it exists + /// + /// + /// + /// + /// + /// + private async Task SetByNotOverrideAsync( + TDocument[] documents, + CancellationToken cancellationToken = default) where TDocument : AutoCompleteDocument + { + var request = new CreateMultiDocumentRequest(_indexName); + foreach (var document in documents) + request.AddDocument(document, document.Id); + + var ret = await _client.CreateMultiDocumentAsync(request, cancellationToken); + return new SetResponse(ret.IsValid, ret.Message) + { + Items = ret.Items.Select(item => new SetResponseItems(item.Id, item.IsValid, item.Message)).ToList() + }; + } +} diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/AutoCompleteFactory.cs b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/AutoCompleteFactory.cs new file mode 100644 index 000000000..34982715e --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/AutoCompleteFactory.cs @@ -0,0 +1,27 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete; + +public class AutoCompleteFactory : IAutoCompleteFactory +{ + private readonly List _relations; + + public AutoCompleteFactory(AutoCompleteRelationsOptions options) => _relations = options.Relations; + + public IAutoCompleteClient CreateClient() + { + var item = _relations.SingleOrDefault(r => r.IsDefault) ?? _relations.FirstOrDefault(); + ArgumentNullException.ThrowIfNull(item, "You should use AddAutoComplete before the project starts"); + return new AutoCompleteClient(item.ElasticClient, item.MasaElasticClient, item.RealIndexName, item.DefaultOperator, item.DefaultSearchType); + } + + /// + /// Create a client corresponding to the index + /// + /// indexName or alias + /// + public IAutoCompleteClient CreateClient(string name) + { + var item = _relations.FirstOrDefault(relation => relation.IndexName == name || relation.Alias == name); + ArgumentNullException.ThrowIfNull(item, nameof(name)); + return new AutoCompleteClient(item.ElasticClient, item.MasaElasticClient, item.RealIndexName, item.DefaultOperator, item.DefaultSearchType); + } +} diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Masa.Contrib.SearchEngine.AutoComplete.csproj b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Masa.Contrib.SearchEngine.AutoComplete.csproj new file mode 100644 index 000000000..3a0435b34 --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Masa.Contrib.SearchEngine.AutoComplete.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Options/AutoCompleteOptions.cs b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Options/AutoCompleteOptions.cs new file mode 100644 index 000000000..e7fd0091f --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Options/AutoCompleteOptions.cs @@ -0,0 +1,71 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete.Options; + +public class AutoCompleteOptions + where TDocument : AutoCompleteDocument +{ + internal string IndexName { get; private set; } + + internal string? Alias { get; private set; } + + internal bool IsDefault { get; private set; } + + internal SearchType DefaultSearchType { get; private set; } = SearchType.Fuzzy; + + internal Operator DefaultOperator { get; private set; } = Operator.Or; + + internal Action>? Action { get; private set; } + + internal Action? IndexSettingAction { get; private set; } + + public AutoCompleteOptions UseIndexName(string indexName) + { + IndexName = indexName; + return this; + } + + /// + /// Set index alias + /// + /// When it is null, no alias is set + /// + public AutoCompleteOptions UseAlias(string alias) + { + Alias = alias; + return this; + } + + /// + /// Set the default AutoComplete + /// + /// + public AutoCompleteOptions UseDefault() + { + IsDefault = true; + return this; + } + + public AutoCompleteOptions UseDefaultSearchType(SearchType defaultSearchType) + { + DefaultSearchType = defaultSearchType; + return this; + } + + public AutoCompleteOptions Mapping(Action> action) + { + Action = action; + return this; + } + + public AutoCompleteOptions IndexSettings(Action indexSettingAction) + { + IndexSettingAction = indexSettingAction; + return this; + } + + public AutoCompleteOptions UseDefaultOperator(Operator defaultOperator) + { + DefaultOperator = defaultOperator; + return this; + } + +} diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Options/AutoCompleteRelationsOptions.cs b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Options/AutoCompleteRelationsOptions.cs new file mode 100644 index 000000000..80d099bbd --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/Options/AutoCompleteRelationsOptions.cs @@ -0,0 +1,50 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete.Options; + +public class AutoCompleteRelationsOptions +{ + internal List Relations = new(); + + public AutoCompleteRelationsOptions AddRelation(AutoCompleteRelations options) + { + Relations.Add(options); + return this; + } +} + +public class AutoCompleteRelations +{ + internal bool IsDefault { get; } + + internal string IndexName { get; } + + internal string? Alias { get; } + + internal string RealIndexName { get; } + + internal Operator DefaultOperator { get; } + + internal IElasticClient ElasticClient { get; } + + internal IMasaElasticClient MasaElasticClient { get; } + + internal SearchType DefaultSearchType { get; } + + internal AutoCompleteRelations( + IElasticClient elasticClient, + IMasaElasticClient masaElasticClient, + string indexName, + string? alias, + bool isDefault, + Operator defaultOperator, + SearchType defaultSearchType) + { + ElasticClient = elasticClient; + MasaElasticClient = masaElasticClient; + IndexName = indexName; + Alias = alias; + RealIndexName = alias ?? indexName; + IsDefault = isDefault; + DefaultOperator = defaultOperator; + DefaultSearchType = defaultSearchType; + } +} diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/README.md b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/README.md new file mode 100644 index 000000000..f1b86a4d5 --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/README.md @@ -0,0 +1,60 @@ +[中](README.zh-CN.md) | EN + +## AutoComplete + +Example: + +```c# +Install-Package Masa.Contrib.SearchEngine.AutoComplete +``` + +Basic usage: + +Using AutoComplete + +```` C# + +string userIndexName = "user_index_01"; +string userAlias = "user_index"; +builder.Services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()) + .AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias)); +```` + +##### Set data + +```` C# +public async Task SetAsync([FromServices] IAutoCompleteClient client) +{ + await client.SetAsync(new AutoCompleteDocument[] + { + new() + { + Text = "Edward Adam Davis", + Value = 1 + }, + new() + { + Text = "Edward Jim", + Value = 1 + } + }); +} +```` + + +##### Get data + +```` C# +public async Task GetAsync([FromServices] IAutoCompleteClient client) +{ + var response = await client.GetAsync("Edward Adam Davis"); + return System.Text.Json.JsonSerializer.Serialize(response); +} +```` + +> Plugins that need to be added by default: +> +> https://github.com/medcl/elasticsearch-analysis-ik +> +> https://github.com/medcl/elasticsearch-analysis-pinyin diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/README.zh-CN.md b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/README.zh-CN.md new file mode 100644 index 000000000..8fb366cdf --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/README.zh-CN.md @@ -0,0 +1,60 @@ +中 | [EN](README.md) + +## AutoComplete + +用例: + +```c# +Install-Package Masa.Contrib.SearchEngine.AutoComplete +``` + +基本用法: + +使用AutoComplete + +``` C# + +string userIndexName = "user_index_01"; +string userAlias = "user_index"; +builder.Services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()) + .AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias)); +``` + +##### 设置数据 + +``` C# +public async Task SetAsync([FromServices] IAutoCompleteClient client) +{ + await client.SetAsync(new AutoCompleteDocument[] + { + new() + { + Text = "Edward Adam Davis", + Value = 1 + }, + new() + { + Text = "Edward Jim", + Value = 1 + } + }); +} +``` + + +##### 获取数据 + +``` C# +public async Task GetAsync([FromServices] IAutoCompleteClient client) +{ + var response = await client.GetAsync("Edward Adam Davis"); + return System.Text.Json.JsonSerializer.Serialize(response); +} +``` + +> 默认需要添加的插件: +> +> https://github.com/medcl/elasticsearch-analysis-ik +> +> https://github.com/medcl/elasticsearch-analysis-pinyin \ No newline at end of file diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/ServiceCollectionExtensions.cs b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..6e607187c --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/ServiceCollectionExtensions.cs @@ -0,0 +1,166 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete; + +public static class ServiceCollectionExtensions +{ + public static MasaElasticsearchBuilder AddAutoComplete(this MasaElasticsearchBuilder builder) + => builder.AddAutoComplete(); + + public static MasaElasticsearchBuilder AddAutoComplete( + this MasaElasticsearchBuilder builder) + => builder.AddAutoComplete, TValue>(); + + public static MasaElasticsearchBuilder AddAutoComplete( + this MasaElasticsearchBuilder builder) + where TDocument : AutoCompleteDocument + { + var indexName = builder.ElasticClient.ConnectionSettings.DefaultIndex; + if (string.IsNullOrEmpty(indexName)) + throw new ArgumentNullException( + nameof(builder.ElasticClient.ConnectionSettings.DefaultIndex), + "The default IndexName is not set"); + + return builder.AddAutoComplete(option => option.UseIndexName(indexName)); + } + + public static MasaElasticsearchBuilder AddAutoComplete( + this MasaElasticsearchBuilder builder, + Action, long>>? action) + => builder.AddAutoComplete(action); + + public static MasaElasticsearchBuilder AddAutoComplete( + this MasaElasticsearchBuilder builder, + Action, TValue>>? action) + => builder.AddAutoComplete, TValue>(action); + + public static MasaElasticsearchBuilder AddAutoComplete( + this MasaElasticsearchBuilder builder, + Action>? action) + where TDocument : AutoCompleteDocument + { + AutoCompleteOptions options = new AutoCompleteOptions(); + action?.Invoke(options); + builder.Services.AddAutoCompleteCore(builder.ElasticClient, builder.Client, options); + return builder; + } + + private static void AddAutoCompleteCore(this IServiceCollection services, + IElasticClient elasticClient, + IMasaElasticClient client, + AutoCompleteOptions option) + where TDocument : AutoCompleteDocument + { + ArgumentNullException.ThrowIfNull(services); + + ArgumentNullException.ThrowIfNull(option.IndexName,nameof(option.IndexName)); + + services.TryAddSingleton(new AutoCompleteRelationsOptions()); + + var autoCompleteRelations = new AutoCompleteRelations(elasticClient, client, option.IndexName, option.Alias, option.IsDefault, option.DefaultOperator, option.DefaultSearchType); + services.TryAddAutoCompleteRelation(autoCompleteRelations); + + services.TryAddSingleton(); + services.TryAddSingleton(serviceProvider => serviceProvider.GetRequiredService().CreateClient()); + client.TryCreateIndexAsync(services.BuildServiceProvider().GetService>(), option); + } + + private static void TryAddAutoCompleteRelation(this IServiceCollection services, AutoCompleteRelations relation) + { + var serviceProvider = services.BuildServiceProvider(); + var relationsOptions = serviceProvider.GetRequiredService(); + + if (relationsOptions.Relations.Any(r => r.Alias == relation.Alias || r.IndexName == relation.IndexName)) + throw new ArgumentException($"indexName or alias exists"); + + if (relation.IsDefault && relationsOptions.Relations.Any(relation => relation.IsDefault)) + throw new ArgumentException(nameof(ElasticsearchRelations.IsDefault), "ElasticClient can only have one default"); + + relationsOptions.AddRelation(relation); + } + + private static void TryCreateIndexAsync( + this IMasaElasticClient client, + ILogger? logger, + AutoCompleteOptions option) + where TDocument : AutoCompleteDocument + { + IAliases? aliases = null; + if (option.Alias != null) + { + aliases = new Aliases(); + aliases.Add(option.Alias, new Alias()); + } + + var existsResponse = client.IndexExistAsync(option.IndexName, CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + if (!existsResponse.IsValid || existsResponse.Exists) + { + if (!existsResponse.IsValid) + logger?.LogError($"AutoComplete: Initialization index is abnormal, {existsResponse.Message}"); + + return; + } + + client.CreateIndex(logger, option.IndexName, aliases, option); + } + + private static void CreateIndex( + this IMasaElasticClient client, + ILogger? logger, + string indexName, + IAliases? aliases, + AutoCompleteOptions option) + where TDocument : AutoCompleteDocument + { + IAnalysis analysis = new AnalysisDescriptor(); + analysis.Analyzers = new Analyzers(); + analysis.TokenFilters = new TokenFilters(); + IIndexSettings indexSettings = new IndexSettings() + { + Analysis = analysis + }; + string analyzer = "ik_max_word_pinyin"; + if (option.IndexSettingAction != null) + option.IndexSettingAction.Invoke(indexSettings); + else + { + string defaultAnalyzer = "ik_max_word"; + string pinyinFilter = "pinyin"; + string wordDelimiterFilter = "word_delimiter"; + indexSettings.Analysis.Analyzers.Add(analyzer, new CustomAnalyzer() + { + Filter = new[] { pinyinFilter, wordDelimiterFilter }, + Tokenizer = defaultAnalyzer + }); + indexSettings.Analysis.TokenFilters.Add(pinyinFilter, new PinYinTokenFilterDescriptor()); + } + + TypeMappingDescriptor mapping = new TypeMappingDescriptor(); + if (option.Action != null) + option.Action.Invoke(mapping); + else + { + mapping = mapping + .AutoMap() + .Properties(ps => + ps.Text(s => + s.Name(n => n.Id) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Text) + .Analyzer(analyzer) + ) + ); + } + + var createIndexResponse = client.CreateIndexAsync(indexName, new CreateIndexOptions() + { + Aliases = aliases, + Mappings = mapping, + IndexSettings = indexSettings + }).ConfigureAwait(false).GetAwaiter().GetResult(); + if (!createIndexResponse.IsValid) + logger?.LogError($"AutoComplete: Initialization index is abnormal, {createIndexResponse.Message}"); + } +} diff --git a/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/_Imports.cs b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/_Imports.cs new file mode 100644 index 000000000..e514082c2 --- /dev/null +++ b/src/SearchEngine/Masa.Contrib.SearchEngine.AutoComplete/_Imports.cs @@ -0,0 +1,15 @@ +global using Masa.BuildingBlocks.SearchEngine.AutoComplete; +global using Masa.BuildingBlocks.SearchEngine.AutoComplete.Options; +global using Masa.BuildingBlocks.SearchEngine.AutoComplete.Response; +global using Masa.Contrib.SearchEngine.AutoComplete.Options; +global using Masa.Utils.Data.Elasticsearch; +global using Masa.Utils.Data.Elasticsearch.Analysis.TokenFilters; +global using Masa.Utils.Data.Elasticsearch.Options.Document.Create; +global using Masa.Utils.Data.Elasticsearch.Options.Document.Query; +global using Masa.Utils.Data.Elasticsearch.Options.Document.Set; +global using Masa.Utils.Data.Elasticsearch.Options.Index; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using Nest; + diff --git a/src/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj b/src/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj index ea0b3de50..575636d26 100644 --- a/src/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj +++ b/src/Service/Masa.Contrib.Service.MinimalAPIs/Masa.Contrib.Service.MinimalAPIs.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj b/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj index ea7704bb0..be1d242e1 100644 --- a/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj +++ b/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/test/Masa.Contrib.Dispatcher.Events.Tests/SagaTest.cs b/test/Masa.Contrib.Dispatcher.Events.Tests/SagaTest.cs index ba72835b8..6630a0fd3 100644 --- a/test/Masa.Contrib.Dispatcher.Events.Tests/SagaTest.cs +++ b/test/Masa.Contrib.Dispatcher.Events.Tests/SagaTest.cs @@ -64,10 +64,14 @@ public async Task TestMultiHandlerBySaga(string account, string optAccount, stri }; if (isError == 1) { - await Assert.ThrowsExceptionAsync(async () => + try { await _eventBus.PublishAsync(@event); - }); + } + catch (Exception ex) + { + Assert.IsTrue(ex.InnerException is NotSupportedException); + } } else { diff --git a/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj b/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj index 06704c6c0..428daff0e 100644 --- a/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj +++ b/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/AutoCompleteTest.cs b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/AutoCompleteTest.cs new file mode 100644 index 000000000..455a503c0 --- /dev/null +++ b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/AutoCompleteTest.cs @@ -0,0 +1,422 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete.Tests; + +[TestClass] +public class AutoCompleteTest +{ + private IServiceCollection _services; + + [TestInitialize] + public void Initialize() + { + _services = new ServiceCollection(); + } + + [TestMethod] + public void TestAddAutoCompleteAndNoIndexName() + { + var builder = _services.AddElasticsearchClient(); + Assert.ThrowsException(() => builder.AddAutoComplete()); + } + + [TestMethod] + public void TestAddAutoComplete() + { + var builder = _services.AddElasticsearchClient("es", option => + { + option.UseConnectionSettings(setting => + { + setting.DefaultIndex("user_index"); + }); + }).AddAutoComplete(); + var serviceProvider = _services.BuildServiceProvider(); + var autoCompleteClient = serviceProvider.GetService(); + Assert.IsNotNull(autoCompleteClient); + + var autoCompleteFactory = serviceProvider.GetService(); + Assert.IsNotNull(autoCompleteFactory); + + var autoCompleteClient2 = autoCompleteFactory.CreateClient(); + Assert.IsNotNull(autoCompleteClient2); + + var elasticClientField = typeof(AutoCompleteClient).GetField("_elasticClient", BindingFlags.Instance | BindingFlags.NonPublic)!; + Assert.IsTrue(elasticClientField.GetValue(autoCompleteClient) == elasticClientField.GetValue(autoCompleteClient2)); + } + + [TestMethod] + public void TestAddMultiAutoComplete() + { + string userIndexName = "user_index_01"; + string userAlias = "user_index"; + + var builder = _services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()) + .AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias)); + Assert.ThrowsException(() => builder.AddAutoComplete(option => option.UseIndexName(userIndexName))); + } + + [TestMethod] + public void TestAddMultiDefaultAutoComplete() + { + string userIndexName = "user_index_01"; + string userAlias = "user_index"; + + var builder = _services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()) + .AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias).UseDefault()); + Assert.ThrowsException(() + => builder.AddAutoComplete(option => option.UseIndexName("employee_index").UseDefault())); + } + + [TestMethod] + public async Task TestGetAsync() + { + string userIndexName = "user_index_01"; + string userAlias = "user_index"; + + var builder = _services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()); + + await builder.Client.DeleteIndexByAliasAsync(userAlias); + + builder.AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias).UseDefaultSearchType(SearchType.Fuzzy)); + + var autoCompleteFactory = builder.Services.BuildServiceProvider().GetRequiredService(); + var autoCompleteClient = autoCompleteFactory.CreateClient(userIndexName); + await autoCompleteClient.SetAsync(new AutoCompleteDocument[] + { + new() + { + Text = "张三", + Value = 1 + }, + new() + { + Text = "李四", + Value = 2 + }, + new() + { + Text = "张丽", + Value = 3 + } + }); + + Thread.Sleep(1000); + + var response = await autoCompleteClient.GetAsync("张三", new AutoCompleteOptions(SearchType.Precise)); + Assert.IsTrue(response.IsValid && response.Total == 1); + + response = await autoCompleteClient.GetAsync("三", new AutoCompleteOptions(SearchType.Precise)); + Assert.IsTrue(response.IsValid && response.Total == 1); + + response = await autoCompleteClient.GetAsync("zs", new AutoCompleteOptions(SearchType.Precise)); + Assert.IsTrue(response.IsValid && response.Total == 1); + + response = await autoCompleteClient.GetAsync("zhang", new AutoCompleteOptions(SearchType.Precise)); + Assert.IsTrue(response.IsValid && response.Total == 2); + + response = await autoCompleteClient.GetAsync("li", new AutoCompleteOptions(SearchType.Precise)); + Assert.IsTrue(response.IsValid && response.Total == 2); + + response = await autoCompleteClient.GetAsync("*si"); + Assert.IsTrue(response.IsValid && response.Total == 1); + + response = await autoCompleteClient.GetAsync("zhang*"); + Assert.IsTrue(response.IsValid && response.Total == 2); + + response = await autoCompleteClient.GetAsync("*"); + Assert.IsTrue(response.IsValid && response.Total == 3); + } + + [TestMethod] + public async Task TestCustomModelAsync() + { + string employeeIndexName = "employee_index_01"; + string employeeAlias = "employee_index"; + var builder = _services.AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200")); + + await builder.Client.DeleteIndexByAliasAsync(employeeAlias); + + string analyzer = "ik_max_word_pinyin"; + builder.AddAutoComplete(option => option + .UseIndexName(employeeIndexName) + .UseAlias(employeeAlias) + .Mapping(descriptor => + { + descriptor.AutoMap() + .Properties(ps => + ps.Text(s => + s.Name(n => n.Id) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Text) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Phone) + .Analyzer(analyzer) + ) + ); + })); + + var autoCompleteFactory = builder.Services.BuildServiceProvider().GetRequiredService(); + var employeeClient = autoCompleteFactory.CreateClient(employeeAlias); + await employeeClient.SetAsync(new Employee[] + { + new() + { + Text = "吉姆", + Value = 1, + Phone = "13999999999" + }, + new() + { + Text = "托尼", + Value = 2, + Phone = "13888888888" + } + }); + + Thread.Sleep(1000); + + var employeeResponse = await employeeClient.GetAsync("吉姆"); + Assert.IsTrue(employeeResponse.IsValid && employeeResponse.Total == 1); + + employeeResponse = await employeeClient.GetAsync("139"); + Assert.IsTrue(employeeResponse.IsValid && employeeResponse.Total == 0); + + employeeResponse = await employeeClient.GetAsync("139*", new AutoCompleteOptions() + { + Field = "phone" + }); + Assert.IsTrue(employeeResponse.IsValid && employeeResponse.Total == 1); + + employeeResponse = await employeeClient.GetAsync("*"); + Assert.IsTrue(employeeResponse.Data.Any(employee => employee.Phone == "13999999999")); + + await employeeClient.SetAsync(new Employee[] + { + new() + { + Text = "吉姆", + Value = 1, + Phone = "13777777777" + } + }); + + Thread.Sleep(1000); + + employeeResponse = await employeeClient.GetAsync("*"); + Assert.IsTrue(employeeResponse.IsValid && employeeResponse.Total == 2); + Assert.IsTrue(employeeResponse.Data.Any(employee => employee.Phone == "13777777777")); + + await employeeClient.SetAsync(new Employee[] + { + new() + { + Text = "吉姆", + Value = 1, + Phone = "13999999999" + } + }, new SetOptions() + { + IsOverride = false + }); + + Thread.Sleep(1000); + + employeeResponse = await employeeClient.GetAsync("*"); + Assert.IsTrue(employeeResponse.Data.Any(employee => employee.Phone == "13777777777")); + } + + [TestMethod] + public async Task TestPreciseAsync() + { + string employeeIndexName = "employee_index_01"; + string employeeAlias = "employee_index"; + var builder = _services.AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200")); + + await builder.Client.DeleteIndexByAliasAsync(employeeAlias); + string analyzer = "ik_max_word_pinyin"; + builder.AddAutoComplete(option => option + .UseIndexName(employeeIndexName) + .UseAlias(employeeAlias) + .Mapping(descriptor => + { + descriptor.AutoMap() + .Properties(ps => + ps.Text(s => + s.Name(n => n.Id) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Text) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Phone) + .Analyzer(analyzer) + ) + ); + })); + + var autoCompleteFactory = builder.Services.BuildServiceProvider().GetRequiredService(); + var employeeClient = autoCompleteFactory.CreateClient(employeeAlias); + await employeeClient.SetAsync(new Employee[] + { + new() + { + Text = "吉姆", + Value = 1, + Phone = "13999999999" + }, + new() + { + Text = "托尼", + Value = 2, + Phone = "13888888888" + } + }); + + Thread.Sleep(1000); + + var employeeResponse = await employeeClient.GetAsync("ji*"); + Assert.IsTrue(employeeResponse.IsValid && employeeResponse.Total == 1); + + employeeResponse = await employeeClient.GetAsync("13999999999", new AutoCompleteOptions(SearchType.Precise) + { + Field = "phone" + }); + Assert.IsTrue(employeeResponse.IsValid && employeeResponse.Total == 1 && + employeeResponse.Data.Any(employee => employee.Value == 1)); + } + + [TestMethod] + public async Task TestOperatorAndAsync() + { + string userIndexName = "user_index_01"; + string userAlias = "user_index"; + + var builder = _services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()) + .AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias).UseDefaultOperator(Nest.Operator.And)); + + await builder.Client.ClearDocumentAsync(userAlias); + + var autoCompleteFactory = builder.Services.BuildServiceProvider().GetRequiredService(); + var autoCompleteClient = autoCompleteFactory.CreateClient(userAlias); + await autoCompleteClient.SetAsync(new AutoCompleteDocument[] + { + new() + { + Text = "Edward Adam Davis", + Value = 1 + }, + new() + { + Text = "Edward Jim", + Value = 1 + } + }); + + Thread.Sleep(1000); + + var response = await autoCompleteClient.GetAsync("Edward Adam Davis"); + Assert.IsTrue(response.IsValid && response.Total == 1); + } + + [TestMethod] + public async Task TestOperatorOrAsync() + { + string userIndexName = "user_index_01"; + string userAlias = "user_index"; + + var builder = _services + .AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200").UseDefault()); + + await builder.Client.DeleteIndexAsync(userIndexName); + + builder.AddAutoComplete(option => option.UseIndexName(userIndexName).UseAlias(userAlias).IndexSettings(setting => + { + string analyzer = "ik_max_word_pinyin"; + IAnalysis analysis = new AnalysisDescriptor(); + analysis.Analyzers = new Analyzers(); + analysis.TokenFilters = new TokenFilters(); + IIndexSettings indexSettings = new IndexSettings() + { + Analysis = analysis + }; + string defaultAnalyzer = "ik_max_word"; + string pinyinFilter = "pinyin"; + string wordDelimiterFilter = "word_delimiter"; + indexSettings.Analysis.Analyzers.Add(analyzer, new CustomAnalyzer() + { + Filter = new[] { pinyinFilter, wordDelimiterFilter }, + Tokenizer = defaultAnalyzer + }); + indexSettings.Analysis.TokenFilters.Add(pinyinFilter, new PinYinTokenFilterDescriptor()); + })); + + var autoCompleteFactory = builder.Services.BuildServiceProvider().GetRequiredService(); + var autoCompleteClient = autoCompleteFactory.CreateClient(userAlias); + await autoCompleteClient.SetAsync(new AutoCompleteDocument[] + { + new() + { + Text = "Edward Adam Davis", + Value = 1 + }, + new() + { + Text = "Edward Jim", + Value = 1 + } + }); + + Thread.Sleep(1000); + + var response = await autoCompleteClient.GetAsync("Edward Adam Davis"); + Assert.IsTrue(response.IsValid && response.Total == 2); + } + + [TestMethod] + public void TestNullIndexName() + { + var builder = _services.AddElasticsearchClient("es", option => option.UseNodes("http://localhost:9200")); + + string analyzer = "ik_max_word_pinyin"; + + Assert.ThrowsException(() => builder.AddAutoComplete(option => option + .Mapping(descriptor => + { + descriptor.AutoMap() + .Properties(ps => + ps.Text(s => + s.Name(n => n.Id) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Text) + .Analyzer(analyzer) + ) + ) + .Properties(ps => + ps.Text(s => + s.Name(n => n.Phone) + .Analyzer(analyzer) + ) + ); + }))); + } +} diff --git a/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/Masa.Contrib.SearchEngine.AutoComplete.Tests.csproj b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/Masa.Contrib.SearchEngine.AutoComplete.Tests.csproj new file mode 100644 index 000000000..4678a2646 --- /dev/null +++ b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/Masa.Contrib.SearchEngine.AutoComplete.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + false + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/Model/Employee.cs b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/Model/Employee.cs new file mode 100644 index 000000000..fb7eed360 --- /dev/null +++ b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/Model/Employee.cs @@ -0,0 +1,6 @@ +namespace Masa.Contrib.SearchEngine.AutoComplete.Tests.Model; + +public class Employee : AutoCompleteDocument +{ + public string Phone { get; set; } +} diff --git a/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/_Imports.cs b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/_Imports.cs new file mode 100644 index 000000000..22f551d4e --- /dev/null +++ b/test/Masa.Contrib.SearchEngine.AutoComplete.Tests/_Imports.cs @@ -0,0 +1,10 @@ +global using Masa.BuildingBlocks.SearchEngine.AutoComplete; +global using Masa.BuildingBlocks.SearchEngine.AutoComplete.Options; +global using Masa.Contrib.SearchEngine.AutoComplete.Tests.Model; +global using Masa.Utils.Data.Elasticsearch; +global using Masa.Utils.Data.Elasticsearch.Analysis.TokenFilters; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Nest; +global using System.Reflection; +