// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Elasticsearch.Net; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Hosting; using Squidex.Infrastructure; namespace Squidex.Extensions.Text.ElasticSearch { public sealed class ElasticSearchTextIndex : ITextIndex, IInitializable { private readonly ElasticLowLevelClient client; private readonly string indexName; private readonly int waitAfterUpdate; public ElasticSearchTextIndex(string configurationString, string indexName, int waitAfterUpdate = 0) { var config = new ConnectionConfiguration(new Uri(configurationString)); client = new ElasticLowLevelClient(config); this.indexName = indexName; this.waitAfterUpdate = waitAfterUpdate; } public Task InitializeAsync( CancellationToken ct) { return ElasticSearchMapping.ApplyAsync(client, indexName, ct); } public Task ClearAsync( CancellationToken ct = default) { return Task.CompletedTask; } public async Task ExecuteAsync(IndexCommand[] commands, CancellationToken ct = default) { var args = new List(); foreach (var command in commands) { CommandFactory.CreateCommands(command, args, indexName); } if (args.Count == 0) { return; } var result = await client.BulkAsync(PostData.MultiJson(args), ctx: ct); if (!result.Success) { throw new InvalidOperationException($"Failed with ${result.Body}", result.OriginalException); } if (waitAfterUpdate > 0) { await Task.Delay(waitAfterUpdate, ct); } } public Task> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, CancellationToken ct = default) { return Task.FromResult>(null); } public async Task> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, CancellationToken ct = default) { Guard.NotNull(app, nameof(app)); Guard.NotNull(query, nameof(query)); var queryText = query.Text; if (string.IsNullOrWhiteSpace(queryText)) { return null; } var isFuzzy = queryText.EndsWith("~", StringComparison.OrdinalIgnoreCase); if (isFuzzy) { queryText = queryText[..^1]; } var field = "texts.*"; if (queryText.Length >= 4 && queryText.IndexOf(":", StringComparison.OrdinalIgnoreCase) == 2) { var candidateLanguage = queryText.Substring(0, 2); if (Language.IsValidLanguage(candidateLanguage)) { field = $"texts.{candidateLanguage}"; queryText = queryText[3..]; } } var serveField = GetServeField(scope); var elasticQuery = new { query = new { @bool = new { must = new List { new { term = new Dictionary { ["appId.keyword"] = app.Id.ToString() } }, new { term = new Dictionary { [serveField] = "true" } }, new { multi_match = new { fuzziness = isFuzzy ? (object)"AUTO" : 0, fields = new[] { field }, query = query.Text } } }, should = new List() } }, _source = new[] { "contentId" }, size = 2000 }; if (query.RequiredSchemaIds?.Count > 0) { var bySchema = new { terms = new Dictionary { ["schemaId.keyword"] = query.RequiredSchemaIds.Select(x => x.ToString()).ToArray() } }; elasticQuery.query.@bool.must.Add(bySchema); } else if (query.PreferredSchemaId.HasValue) { var bySchema = new { terms = new Dictionary { ["schemaId.keyword"] = query.PreferredSchemaId.ToString() } }; elasticQuery.query.@bool.should.Add(bySchema); } var result = await client.SearchAsync(indexName, CreatePost(elasticQuery), ctx: ct); if (!result.Success) { throw result.OriginalException; } var ids = new List(); foreach (var item in result.Body.hits.hits) { if (item != null) { ids.Add(DomainId.Create(item["_source"]["contentId"])); } } return ids; } private static string GetServeField(SearchScope scope) { return scope == SearchScope.Published ? "servePublished" : "serveAll"; } private static PostData CreatePost(T data) { return new SerializableData(data); } } }