mirror of https://github.com/Squidex/squidex.git
13 changed files with 324 additions and 63 deletions
@ -0,0 +1,78 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Elasticsearch.Net; |
||||
|
|
||||
|
namespace Squidex.Extensions.Text.ElasticSearch |
||||
|
{ |
||||
|
internal class ElasticSearchClient : IElasticSearchClient |
||||
|
{ |
||||
|
private readonly IElasticLowLevelClient elasticSearch; |
||||
|
|
||||
|
public ElasticSearchClient(string configurationString) |
||||
|
{ |
||||
|
var config = new ConnectionConfiguration(new Uri(configurationString)); |
||||
|
|
||||
|
elasticSearch = new ElasticLowLevelClient(config); |
||||
|
} |
||||
|
|
||||
|
public async Task CreateIndexAsync<T>(string indexName, T request, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var result = await elasticSearch.Indices.PutMappingAsync<StringResponse>(indexName, CreatePost(request), ctx: ct); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task BulkAsync<T>(List<T> requests, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var result = await elasticSearch.BulkAsync<StringResponse>(CreatePost(requests), ctx: ct); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<List<dynamic>> SearchAsync<T>(string indexName, T request, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var result = await elasticSearch.SearchAsync<DynamicResponse>(indexName, CreatePost(request), ctx: ct); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw result.OriginalException; |
||||
|
} |
||||
|
|
||||
|
var hits = new List<dynamic>(); |
||||
|
|
||||
|
foreach (var item in result.Body.hits.hits) |
||||
|
{ |
||||
|
if (item != null) |
||||
|
{ |
||||
|
hits.Add(item); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return hits; |
||||
|
} |
||||
|
|
||||
|
private static PostData CreatePost<T>(List<T> requests) |
||||
|
{ |
||||
|
return PostData.MultiJson(requests.OfType<object>()); |
||||
|
} |
||||
|
|
||||
|
private static PostData CreatePost<T>(T data) |
||||
|
{ |
||||
|
return new SerializableData<T>(data); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Extensions.Text.ElasticSearch |
||||
|
{ |
||||
|
public interface IElasticSearchClient |
||||
|
{ |
||||
|
Task CreateIndexAsync<T>(string indexName, T request, |
||||
|
CancellationToken ct); |
||||
|
|
||||
|
Task BulkAsync<T>(List<T> requests, |
||||
|
CancellationToken ct); |
||||
|
|
||||
|
Task<List<dynamic>> SearchAsync<T>(string indexName, T request, |
||||
|
CancellationToken ct); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using OpenSearch.Net; |
||||
|
|
||||
|
namespace Squidex.Extensions.Text.ElasticSearch |
||||
|
{ |
||||
|
public sealed class OpenSearchClient : IElasticSearchClient |
||||
|
{ |
||||
|
private readonly IOpenSearchLowLevelClient openSearch; |
||||
|
|
||||
|
public OpenSearchClient(string configurationString) |
||||
|
{ |
||||
|
var config = new ConnectionConfiguration(new Uri(configurationString)); |
||||
|
|
||||
|
openSearch = new OpenSearchLowLevelClient(config); |
||||
|
} |
||||
|
|
||||
|
public async Task CreateIndexAsync<T>(string indexName, T request, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var result = await openSearch.Indices.PutMappingAsync<StringResponse>(indexName, CreatePost(request), ctx: ct); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task BulkAsync<T>(List<T> requests, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var result = await openSearch.BulkAsync<StringResponse>(CreatePost(requests), ctx: ct); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<List<dynamic>> SearchAsync<T>(string indexName, T request, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
var result = await openSearch.SearchAsync<DynamicResponse>(indexName, CreatePost(request), ctx: ct); |
||||
|
|
||||
|
if (!result.Success) |
||||
|
{ |
||||
|
throw result.OriginalException; |
||||
|
} |
||||
|
|
||||
|
var hits = new List<dynamic>(); |
||||
|
|
||||
|
foreach (var item in result.Body.hits.hits) |
||||
|
{ |
||||
|
if (item != null) |
||||
|
{ |
||||
|
hits.Add(item); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return hits; |
||||
|
} |
||||
|
|
||||
|
private static PostData CreatePost<T>(List<T> requests) |
||||
|
{ |
||||
|
return PostData.MultiJson(requests.OfType<object>()); |
||||
|
} |
||||
|
|
||||
|
private static PostData CreatePost<T>(T data) |
||||
|
{ |
||||
|
return new SerializableData<T>(data); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Core.TestHelpers; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Extensions.Text.ElasticSearch; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
public sealed class ElasticSearchTextIndexFixture : IAsyncLifetime |
||||
|
{ |
||||
|
public ElasticSearchTextIndex Index { get; } |
||||
|
|
||||
|
public ElasticSearchTextIndexFixture() |
||||
|
{ |
||||
|
Index = new ElasticSearchTextIndex( |
||||
|
new ElasticSearchClient(TestConfig.Configuration["elastic:configuration"]), |
||||
|
TestConfig.Configuration["elastic:indexName"], |
||||
|
TestUtils.DefaultSerializer); |
||||
|
} |
||||
|
|
||||
|
public Task InitializeAsync() |
||||
|
{ |
||||
|
return Index.InitializeAsync(default); |
||||
|
} |
||||
|
|
||||
|
public Task DisposeAsync() |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Text |
||||
|
{ |
||||
|
[Trait("Category", "Dependencies")] |
||||
|
public class ElasticSearchTextIndexTests : TextIndexerTestsBase, IClassFixture<ElasticSearchTextIndexFixture> |
||||
|
{ |
||||
|
public override bool SupportsGeo => true; |
||||
|
|
||||
|
public override int WaitAfterUpdate => 2000; |
||||
|
|
||||
|
public ElasticSearchTextIndexFixture _ { get; } |
||||
|
|
||||
|
public ElasticSearchTextIndexTests(ElasticSearchTextIndexFixture fixture) |
||||
|
{ |
||||
|
_ = fixture; |
||||
|
} |
||||
|
|
||||
|
public override ITextIndex CreateIndex() |
||||
|
{ |
||||
|
return _.Index; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_retrieve_english_stopword_only_for_german_query() |
||||
|
{ |
||||
|
await CreateTextAsync(ids1[0], "de", "and y"); |
||||
|
await CreateTextAsync(ids2[0], "en", "and y"); |
||||
|
|
||||
|
await SearchText(expected: ids2, text: "und"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_retrieve_german_stopword_only_for_english_query() |
||||
|
{ |
||||
|
await CreateTextAsync(ids1[0], "de", "and und"); |
||||
|
await CreateTextAsync(ids2[0], "en", "and und"); |
||||
|
|
||||
|
await SearchText(expected: ids1, text: "and"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_index_cjk_content_and_retrieve() |
||||
|
{ |
||||
|
await CreateTextAsync(ids1[0], "zh", "東京大学"); |
||||
|
|
||||
|
await SearchText(expected: ids1, text: "東京"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue