// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Text.RegularExpressions; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; namespace Squidex.Extensions.Text.ElasticSearch; public sealed partial class ElasticSearchTextIndex(IElasticSearchClient elasticClient, string indexName, IJsonSerializer jsonSerializer) : ITextIndex, IInitializable { private static readonly Regex RegexLanguageNormal = BuildLanguageRegexNormal(); private static readonly Regex RegexLanguageStart = BuildLanguageRegexStart(); private readonly QueryParser queryParser = new QueryParser(ElasticSearchIndexDefinition.GetFieldPath); public Task InitializeAsync( CancellationToken ct) { return ElasticSearchIndexDefinition.ApplyAsync(elasticClient, indexName, ct); } public Task ClearAsync( CancellationToken ct = default) { return Task.CompletedTask; } public 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 Task.CompletedTask; } return elasticClient.BulkAsync(args, ct); } public async Task?> SearchAsync(App app, GeoQuery query, SearchScope scope, CancellationToken ct = default) { Guard.NotNull(app); Guard.NotNull(query); var serveField = GetServeField(scope); var elasticQuery = new { query = new { @bool = new { filter = new object[] { new { term = new Dictionary { ["schemaId.keyword"] = query.SchemaId.ToString(), }, }, new { term = new Dictionary { ["geoField.keyword"] = query.Field, }, }, new { term = new Dictionary { [serveField] = "true", }, }, new { geo_distance = new { geoObject = new { lat = query.Latitude, lon = query.Longitude, }, distance = $"{query.Radius}m", }, }, }, }, }, _source = new[] { "contentId", }, size = query.Take, }; return await SearchAsync(elasticQuery, ct); } public async Task?> SearchAsync(App app, TextQuery query, SearchScope scope, CancellationToken ct = default) { Guard.NotNull(app); Guard.NotNull(query); var parsed = queryParser.Parse(query.Text); if (parsed == null) { return null; } var serveField = GetServeField(scope); var elasticQuery = new { query = new { @bool = new { filter = new List { new { term = new Dictionary { ["appId.keyword"] = app.Id.ToString(), }, }, new { term = new Dictionary { [serveField] = "true", }, }, }, must = new { query_string = new { query = parsed.Text, }, }, should = new List(), }, }, _source = new[] { "contentId", }, size = query.Take, }; if (query.RequiredSchemaIds?.Count > 0) { var bySchema = new { terms = new Dictionary { ["schemaId.keyword"] = query.RequiredSchemaIds.Select(x => x.ToString()).ToArray(), }, }; elasticQuery.query.@bool.filter.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 json = jsonSerializer.Serialize(elasticQuery, true); return await SearchAsync(elasticQuery, ct); } public async Task FindUserInfo(App app, ApiKeyQuery query, SearchScope scope, CancellationToken ct = default) { Guard.NotNull(app); Guard.NotNull(query); var serveField = GetServeField(scope); var elasticQuery = new { query = new { @bool = new { filter = new object[] { new { term = new Dictionary { [serveField] = "true", }, }, new { term = new Dictionary { ["userInfoApiKey.keyword"] = query.ApiKey, }, }, }, }, }, _source = new[] { "contentId", "userInfoApiKey", "userInfoRole", }, size = 1, }; var hits = await elasticClient.SearchAsync(indexName, elasticQuery, ct); var hit = hits.FirstOrDefault(); return hit != null ? new UserInfoResult(DomainId.Create(hit["_source"]["contentId"]), hit["_source"]["userInfoRole"]) : null; } private async Task> SearchAsync(object query, CancellationToken ct) { var hits = await elasticClient.SearchAsync(indexName, query, ct); var ids = new List(); foreach (var item in hits) { ids.Add(DomainId.Create(item["_source"]["contentId"])); } return ids; } private static string GetServeField(SearchScope scope) { return scope == SearchScope.Published ? "servePublished" : "serveAll"; } [GeneratedRegex("[^\\w]+([a-z\\-_]{2,}):", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] private static partial Regex BuildLanguageRegexNormal(); [GeneratedRegex("$^([a-z\\-_]{2,}):", RegexOptions.ExplicitCapture | RegexOptions.Compiled)] private static partial Regex BuildLanguageRegexStart(); }