Initial code commit

This commit is contained in:
2022-12-06 09:03:41 +01:00
parent d106d21e7d
commit 49d6401bfa
374 changed files with 48980 additions and 0 deletions
@@ -0,0 +1,117 @@
using System;
using System.Reflection;
using Autofac;
using FluentValidation;
using Microsoft.Extensions.Caching.Memory;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework;
using Rsdo.Concordancer.Services.Framework.BulkLoaders;
using Rsdo.Concordancer.Services.Framework.Cache;
using Rsdo.Concordancer.Services.Framework.DatabaseManager;
using Rsdo.Concordancer.Services.Framework.DbContext;
using Rsdo.Concordancer.Services.Framework.Decorators;
using Rsdo.Concordancer.Services.Search.Aggregations;
using Rsdo.Concordancer.Services.Search.AlternateSearches;
using Rsdo.Concordancer.Services.Search.QueryFactories;
using Rsdo.Concordancer.Services.Services.InputQueryParser;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
using Rsdo.Concordancer.Services.Services.ParagraphService;
using Rsdo.Concordancer.Services.Services.PartOfSpeechService;
using Rsdo.Concordancer.Services.Services.TokenizerService;
using Module = Autofac.Module;
namespace Rsdo.Concordancer.Services.CompositionRoot;
public class ServicesModule : Module
{
private Assembly ServicesAssembly => GetType().Assembly;
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
RegisterCache(builder);
RegisterDatabase(builder);
RegisterMediator(builder);
RegisterServiceBus(builder);
RegisterRequestHandlers(builder);
RegisterSearch(builder);
RegisterServices(builder);
}
private static void RegisterCache(ContainerBuilder builder)
{
var memoryCache = new MemoryCache(new MemoryCacheOptions());
builder.RegisterInstance(memoryCache).As<IMemoryCache>().SingleInstance();
builder.RegisterType<PartOfSpeechCacheWarmUp>().As<ICacheWarmUp>().InstancePerDependency();
}
private static void RegisterDatabase(ContainerBuilder builder)
{
// Temporary switch
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
// Database manager
builder.RegisterType<PostgreSqlDatabaseManager>().As<IDatabaseManager>().SingleInstance();
// Db contexts
builder.RegisterType<MasterDbContext>().InstancePerLifetimeScope();
builder.RegisterType<CorpusDbContext>().InstancePerLifetimeScope();
// Bulk loaders
builder.RegisterType<PostgreSqlBulkLoader>().As<IBulkLoader>().InstancePerLifetimeScope();
}
private static void RegisterMediator(ContainerBuilder builder)
{
// Mediator
builder.RegisterType<Mediator>().As<IMediator>().InstancePerLifetimeScope();
}
private static void RegisterServiceBus(ContainerBuilder builder)
{
// Service bus
builder.RegisterType<ServiceBus>().As<IServiceBus>().InstancePerLifetimeScope();
}
private static void RegisterServices(ContainerBuilder builder)
{
builder.RegisterType<ClasslaTokenizerService>().Keyed<ITokenizerService>(TokenizerType.Classla).SingleInstance();
builder.RegisterType<DefaultTokenizerService>().Keyed<ITokenizerService>(TokenizerType.Default).SingleInstance();
builder.RegisterType<InputQueryParser>().As<IInputQueryParser>().SingleInstance();
builder.RegisterType<ParagraphService>().As<IParagraphService>().InstancePerLifetimeScope();
builder.RegisterType<LemmatizationService>().As<ILemmatizationService>().InstancePerLifetimeScope();
builder.RegisterType<PartOfSpeechService>().As<IPartOfSpeechService>().InstancePerLifetimeScope();
}
private void RegisterRequestHandlers(ContainerBuilder builder)
{
// Validators
builder.RegisterAssemblyTypes(ServicesAssembly).AsClosedTypesOf(typeof(IValidator<>)).AsImplementedInterfaces().InstancePerLifetimeScope();
// Request handlers
builder.RegisterAssemblyTypes(ServicesAssembly).AsClosedTypesOf(typeof(IRequestHandler<,>)).AsImplementedInterfaces();
builder.RegisterGenericDecorator(typeof(SearchConcordancesDecorator<,>), typeof(IRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(CurrentContextInitializationDecorator<,>), typeof(IRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(LoggingDecorator<,>), typeof(IRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(RequestValidationDecorator<,>), typeof(IRequestHandler<,>));
}
private void RegisterSearch(ContainerBuilder builder)
{
// Query factories
builder.RegisterAssemblyTypes(ServicesAssembly).AsClosedTypesOf(typeof(IQueryFactory<,>)).AsImplementedInterfaces().InstancePerLifetimeScope();
// Aggregations
builder.RegisterType<AggregationProviderFactory>().As<IAggregationProviderFactory>().InstancePerLifetimeScope();
builder.RegisterType<TextAggregationProvider>().Keyed<IAggregationProvider>(AggregationType.Text).InstancePerLifetimeScope();
// Alternate searches
builder.RegisterType<LemmasAlternateSearchProvider>()
.As<IAlternateSearchProvider<SearchConcordances, SearchConcordancesResponse>>()
.InstancePerLifetimeScope();
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Extensions;
public static class HighlighterExtensions
{
public static void Highlight(this List<ConcordanceToken> tokens, ConcordancesQuery query, int centerTokenIndex)
{
var wordsInContextIndexes = query.WordsInContext.IsNullOrEmpty()
? new HashSet<int>()
: GetWordsInContextIndexes(tokens, query, centerTokenIndex).ToHashSet();
for (var i = 0; i < tokens.Count; i++)
{
// Center word
if (i == centerTokenIndex)
{
tokens[i].IsCenterMatch = true;
}
if (wordsInContextIndexes.Contains(i))
{
tokens[i].IsWordInContextMatch = true;
}
}
}
private static bool CheckCandidateToken(List<ConcordanceToken> tokens, ConcordancesQuery query, int relativeTokenIndex, int absoluteTokenIndex)
{
foreach (var wordInContext in query.WordsInContext)
{
// Try to find candidates only on positive criteria
if (wordInContext.ConditionType != ConditionType.Is)
{
continue;
}
var candidate = tokens[absoluteTokenIndex];
if (!wordInContext.Positions.Contains(relativeTokenIndex))
{
continue;
}
if (!wordInContext.Form.IsNullOrEmpty())
{
if (!candidate.Form.Equals(wordInContext.Form, StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
if (!wordInContext.Lemmas.IsNullOrEmpty())
{
if (!wordInContext.Lemmas.Any(l => l.Equals(candidate.Lemma, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
}
return true;
}
return false;
}
private static IEnumerable<int> GetWordsInContextIndexes(List<ConcordanceToken> tokens, ConcordancesQuery query, int centerTokenIndex)
{
// Check up to 10 tokens to the right, since we cannot search in the left context of the main word
var candidateTokenIndices = new List<int>();
for (var i = centerTokenIndex + 1; i < tokens.Count; i++)
{
if (candidateTokenIndices.Count >= 10)
{
break;
}
if (tokens[i].Type is TokenType.Word or TokenType.PunctuationCharacter)
{
candidateTokenIndices.Add(i);
}
}
for (var i = 0; i < candidateTokenIndices.Count; i++)
{
if (CheckCandidateToken(tokens, query, i + 1, candidateTokenIndices[i]))
{
yield return candidateTokenIndices[i];
}
}
}
}
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
namespace Rsdo.Concordancer.Services.Framework.BulkLoaders;
public interface IBulkLoader
{
Task InsertEntities<TEntity>(List<TEntity> entities, CancellationToken cancellationToken)
where TEntity : Entity;
Task InsertEntities<TEntity>(List<TEntity> entities, bool loadAutoIds, CancellationToken cancellationToken)
where TEntity : Entity;
}
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Framework.BulkLoaders;
public class PostgreSqlBulkLoader : IBulkLoader
{
private readonly CorpusDbContext corpusDbContext;
private readonly MasterDbContext masterDbContext;
public PostgreSqlBulkLoader(CorpusDbContext corpusDbContext, MasterDbContext masterDbContext)
{
this.corpusDbContext = corpusDbContext;
this.masterDbContext = masterDbContext;
}
public Task InsertEntities<TEntity>(List<TEntity> entities, CancellationToken cancellationToken)
where TEntity : Entity
{
return InsertEntities(entities, true, cancellationToken);
}
public async Task InsertEntities<TEntity>(List<TEntity> entities, bool loadAutoIds, CancellationToken cancellationToken)
where TEntity : Entity
{
// Get table and field names
var entityType = GetEntityType<TEntity>();
var tableName = entityType.GetTableName();
var storeObjectIdentifier = GetTableIdentifier(entityType);
var keyProperties = entityType.FindPrimaryKey().Properties;
var dataProperties = entityType.GetDeclaredProperties()
.Where(x => keyProperties.All(kp => kp.Name != x.Name) && x.GetColumnName(storeObjectIdentifier) != null)
.ToList();
var dataFieldNames = dataProperties.Select(p => p.GetColumnName(storeObjectIdentifier)).ToList();
var copyFromCommand = $"COPY {tableName} ({string.Join(",", dataFieldNames)}) FROM STDIN (FORMAT BINARY)";
var dbContext = GetDbContext<TEntity>();
await using var connection = new NpgsqlConnection(dbContext.Database.GetConnectionString());
// Load data into table
await connection.OpenAsync(cancellationToken);
await using (var writer = await connection.BeginBinaryImportAsync(copyFromCommand, cancellationToken))
{
foreach (var entity in entities)
{
await writer.StartRowAsync(cancellationToken);
var values = dbContext.Entry(entity).CurrentValues;
foreach (var dataProperty in dataProperties)
{
var value = values[dataProperty];
if (value == null)
{
await writer.WriteNullAsync(cancellationToken);
}
else
{
if (dataProperty.ClrType.IsEnum)
{
// Try to cast enums to byte
value = Convert.ToByte(value);
}
await writer.WriteAsync(value, cancellationToken);
}
}
}
await writer.CompleteAsync(cancellationToken);
}
if (loadAutoIds)
{
await GetEntityIdentifiers(entities, connection);
}
}
private static StoreObjectIdentifier GetTableIdentifier(IReadOnlyEntityType entityType)
{
var tableName = entityType.GetTableName();
var schema = entityType.GetSchema();
return StoreObjectIdentifier.Table(tableName!, schema);
}
private async Task GetEntityIdentifiers<TEntity>(List<TEntity> entities, NpgsqlConnection connection)
where TEntity : Entity
{
// Get table and field names
var entityType = GetEntityType<TEntity>();
var tableName = entityType.GetTableName();
var storeObjectIdentifier = GetTableIdentifier(entityType);
var autoIdFieldName = entityType.GetProperty(nameof(Entity.AutoId)).GetColumnName(storeObjectIdentifier);
var entityIdFieldName = entityType.GetProperty(nameof(Entity.Id)).GetColumnName(storeObjectIdentifier);
// Get all Id->AutoId mappings
var autoIds = new Dictionary<Guid, long>();
foreach (var chunk in entities.Chunk(1000))
{
var ids = string.Join(",", chunk.Select(e => "'" + e.Id + "'"));
var sql = $"SELECT {autoIdFieldName}, {entityIdFieldName} FROM {tableName} WHERE {entityIdFieldName} IN ({ids})";
await using var command = new NpgsqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
autoIds.Add(reader.GetGuid(1), reader.GetInt64(0));
}
}
// Update entity AutoIds
foreach (var entity in entities)
{
entity.AutoId = autoIds[entity.Id];
}
}
private Microsoft.EntityFrameworkCore.DbContext GetDbContext<TEntity>()
{
return corpusDbContext.Model.FindEntityType(typeof(TEntity)) != null ? corpusDbContext : masterDbContext;
}
private IEntityType GetEntityType<TEntity>()
{
var entityType = corpusDbContext.Model.FindEntityType(typeof(TEntity)) ?? masterDbContext.Model.FindEntityType(typeof(TEntity));
if (entityType == null)
{
throw new Exception($"Entity type {typeof(TEntity).FullName} is not mapped in database context!");
}
return entityType;
}
}
@@ -0,0 +1,6 @@
namespace Rsdo.Concordancer.Services.Framework.Cache;
public interface ICacheWarmUp
{
void WarmUp();
}
@@ -0,0 +1,34 @@
using System.Linq;
using Microsoft.Extensions.Caching.Memory;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Framework.Cache;
public class PartOfSpeechCacheWarmUp : ICacheWarmUp
{
private readonly MasterDbContext dbContext;
private readonly IMemoryCache memoryCache;
public PartOfSpeechCacheWarmUp(MasterDbContext dbContext, IMemoryCache memoryCache)
{
this.dbContext = dbContext;
this.memoryCache = memoryCache;
}
public void WarmUp()
{
CacheMsds();
}
private void CacheMsds()
{
// Get all msds
var msds = dbContext.Msd.ToList();
// Cache individual msd by code
foreach (var msd in msds)
{
memoryCache.Set(Core.Constants.Cache.CacheKeys.Msd.ByCode(msd.Code), msd, Core.Constants.Cache.Duration.Long);
}
}
}
@@ -0,0 +1,17 @@
using System;
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Services.Framework.DatabaseManager;
public interface IDatabaseManager
{
Task CreateCorpusDatabase();
Task CreateMasterDatabase(string connectionString, string databaseName);
Task DeleteCorpusDatabase();
Task UpdateCorpusDatabases();
Task UpdateMasterDatabase();
}
@@ -0,0 +1,124 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Framework.DatabaseManager;
public class PostgreSqlDatabaseManager : IDatabaseManager
{
private readonly IConnectionStringProvider connectionStringProvider;
private readonly IDatabaseMigrationRunner databaseMigrationRunner;
private readonly MasterDbContext dbContext;
public PostgreSqlDatabaseManager(
IConnectionStringProvider connectionStringProvider,
IDatabaseMigrationRunner databaseMigrationRunner,
MasterDbContext dbContext)
{
this.connectionStringProvider = connectionStringProvider;
this.databaseMigrationRunner = databaseMigrationRunner;
this.dbContext = dbContext;
}
public async Task CreateCorpusDatabase()
{
// Create database
var connectionString = connectionStringProvider.GetMasterConnectionString();
var databaseName = connectionStringProvider.GetCorpusDatabaseName();
await CreateDatabase(connectionString, databaseName);
// Run migrations
connectionString = connectionStringProvider.GetCorpusConnectionString();
databaseMigrationRunner.MigrateUp(connectionString, MigrationTag.Corpus);
}
public async Task CreateMasterDatabase(string connectionString, string databaseName)
{
// Create database
await CreateDatabase(connectionString, databaseName);
// Run migrations
var connectionStringWithDatabase = GetConnectionStringWithDatabase(connectionString, databaseName);
databaseMigrationRunner.MigrateUp(connectionStringWithDatabase, MigrationTag.Master);
}
public async Task DeleteCorpusDatabase()
{
// Delete database
var connectionString = connectionStringProvider.GetMasterConnectionString();
var databaseName = connectionStringProvider.GetCorpusDatabaseName();
await DeleteDatabase(connectionString, databaseName);
}
public async Task UpdateCorpusDatabases()
{
// Update all corpus databases
var corpusIds = await dbContext.Corpus.Select(c => c.Id).ToListAsync();
foreach (var corpusId in corpusIds)
{
var connectionString = connectionStringProvider.GetCorpusConnectionString(corpusId);
databaseMigrationRunner.MigrateUp(connectionString, MigrationTag.Corpus);
}
}
public Task UpdateMasterDatabase()
{
// Update master database
var connectionString = connectionStringProvider.GetMasterConnectionString();
databaseMigrationRunner.MigrateUp(connectionString, MigrationTag.Master);
return Task.CompletedTask;
}
private static async Task CreateDatabase(string connectionString, string databaseName)
{
// Open connection
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
// Check if database already exists
var databaseExists = await DatabaseExists(connection, databaseName);
if (databaseExists)
{
throw new Exception($"Database {databaseName} already exists!");
}
// Create database
var sql = $"CREATE DATABASE \"{databaseName}\" ENCODING = 'UTF8'";
await using var command = new NpgsqlCommand(sql, connection);
await command.ExecuteNonQueryAsync();
}
private static async Task<bool> DatabaseExists(NpgsqlConnection connection, string databaseName)
{
var sql = $"SELECT datname FROM pg_catalog.pg_database WHERE datname = @databaseName";
await using var command = new NpgsqlCommand(sql, connection);
command.Parameters.AddWithValue("databaseName", databaseName);
await using var reader = await command.ExecuteReaderAsync();
return reader.HasRows;
}
private static async Task DeleteDatabase(string connectionString, string databaseName)
{
// Open connection
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
// Drop database
var sql = $"DROP DATABASE IF EXISTS \"{databaseName}\" WITH (FORCE)";
await using var command = new NpgsqlCommand(sql, connection);
await command.ExecuteNonQueryAsync();
}
private static string GetConnectionStringWithDatabase(string connectionString, string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(connectionString)
{
Database = databaseName,
};
return builder.ToString();
}
}
@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Interfaces;
namespace Rsdo.Concordancer.Services.Framework.DbContext;
public class CorpusDbContext : Microsoft.EntityFrameworkCore.DbContext
{
private readonly IConnectionStringProvider connectionStringProvider;
public CorpusDbContext(IConnectionStringProvider connectionStringProvider)
{
this.connectionStringProvider = connectionStringProvider;
}
public DbSet<Paragraph> Paragraph { get; set; }
public DbSet<Sentence> Sentence { get; set; }
public DbSet<Term> Term { get; set; }
public DbSet<TermList> TermList { get; set; }
public DbSet<Text> Text { get; set; }
public DbSet<Token> Token { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseNpgsql(connectionStringProvider.GetCorpusConnectionString()).UseLowerCaseNamingConvention();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Paragraph>().HasKey(e => e.AutoId);
modelBuilder.Entity<Sentence>().HasKey(e => e.AutoId);
modelBuilder.Entity<Term>().HasKey(e => e.AutoId);
modelBuilder.Entity<TermList>().HasKey(e => e.AutoId);
modelBuilder.Entity<Text>().HasKey(e => e.AutoId);
modelBuilder.Entity<Token>().HasKey(e => e.AutoId);
}
}
@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Interfaces;
namespace Rsdo.Concordancer.Services.Framework.DbContext;
public class MasterDbContext : Microsoft.EntityFrameworkCore.DbContext
{
private readonly IConnectionStringProvider connectionStringProvider;
public MasterDbContext(IConnectionStringProvider connectionStringProvider)
{
this.connectionStringProvider = connectionStringProvider;
}
public DbSet<Corpus> Corpus { get; set; }
public DbSet<LemmaFormPair> LemmaFormPair { get; set; }
public DbSet<Msd> Msd { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseNpgsql(connectionStringProvider.GetMasterConnectionString()).UseLowerCaseNamingConvention();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Corpus>().HasKey(e => e.AutoId);
modelBuilder.Entity<LemmaFormPair>().HasKey(e => e.AutoId);
modelBuilder.Entity<Msd>().HasKey(e => e.AutoId);
}
}
@@ -0,0 +1,58 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Framework;
using Rsdo.Concordancer.Core.Framework.CurrentContexts;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Framework.Decorators;
public class CurrentContextInitializationDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> decoratedHandler;
private readonly MasterDbContext dbContext;
public CurrentContextInitializationDecorator(IRequestHandler<TRequest, TResponse> decoratedHandler, MasterDbContext dbContext)
{
this.decoratedHandler = decoratedHandler;
this.dbContext = dbContext;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
// Check if current context is set-up
CurrentContext.Current ??= new DefaultCurrentContext();
// Get corpus ID
if (request is IHaveCorpusId haveCorpus)
{
await ValidateCorpus(request, haveCorpus.CorpusId);
CurrentContext.Current.CorpusId = haveCorpus.CorpusId;
}
return await decoratedHandler.Handle(request, cancellationToken);
}
private async Task ValidateCorpus(TRequest request, Guid corpusId)
{
// Corpus must exist
var corpus = await dbContext.Corpus.SingleOrDefaultAsync(c => c.Id == corpusId);
if (corpus == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Corpus, corpusId));
}
// Corpus status must be active
if (request is not (CreateCorpusStore or DeleteCorpusStore) && corpus.Status != CorpusStatus.Active)
{
throw new XForbiddenException(Errors.Forbidden.CorpusStatusIsNotValid(corpus.Status, CorpusStatus.Active));
}
}
}
@@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Services.Framework.Decorators;
public class LoggingDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> decoratedHandler;
private readonly ILogger<TRequest> logger;
public LoggingDecorator(IRequestHandler<TRequest, TResponse> decoratedHandler, ILogger<TRequest> logger)
{
this.decoratedHandler = decoratedHandler;
this.logger = logger;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
logger.LogInformation("Calling handler {handler} to handle {@request}.", decoratedHandler.GetType().ToString(), request);
return await decoratedHandler.Handle(request, cancellationToken);
}
}
@@ -0,0 +1,32 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using FluentValidation;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Services.Framework.Decorators;
public class RequestValidationDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> decoratedHandler;
private readonly ILifetimeScope lifetimeScope;
public RequestValidationDecorator(IRequestHandler<TRequest, TResponse> decoratedHandler, ILifetimeScope lifetimeScope)
{
this.decoratedHandler = decoratedHandler;
this.lifetimeScope = lifetimeScope;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
if (lifetimeScope.TryResolve<IValidator<TRequest>>(out var validator))
{
await validator.ValidateAndThrowAsync(request, cancellationToken);
}
return await decoratedHandler.Handle(request, cancellationToken);
}
}
@@ -0,0 +1,36 @@
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.Services.Services.InputQueryParser;
namespace Rsdo.Concordancer.Services.Framework.Decorators;
public class SearchConcordancesDecorator<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> decoratedHandler;
private readonly IInputQueryParser inputQueryParser;
public SearchConcordancesDecorator(IRequestHandler<TRequest, TResponse> decoratedHandler, IInputQueryParser inputQueryParser)
{
this.decoratedHandler = decoratedHandler;
this.inputQueryParser = inputQueryParser;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
{
if (request is BaseSearchConcordances<TResponse> search)
{
if (!string.IsNullOrEmpty(search.Query))
{
var parsed = await inputQueryParser.Parse(search.Query);
search.MainWord = parsed.mainWord;
search.WordsInContext = parsed.wordsInContext;
}
}
return await decoratedHandler.Handle(request, cancellationToken);
}
}
@@ -0,0 +1,35 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Autofac;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Services.Framework;
public class Mediator : IMediator
{
private readonly ILifetimeScope lifetimeScope;
public Mediator(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
return SendInternal(request, cancellationToken);
}
public Task<TResponse> Send<TResponse>(string requestName, IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
return SendInternal(request, cancellationToken);
}
private Task<TResponse> SendInternal<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
var handlerType = typeof(IRequestHandler<,>).MakeGenericType(request.GetType(), typeof(TResponse));
var handler = (dynamic)lifetimeScope.Resolve(handlerType);
return handler.Handle((dynamic)request, cancellationToken);
}
}
@@ -0,0 +1,18 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Services.Framework;
public class ServiceBus : IServiceBus
{
public Task Send<TResponse>(IRequest<TResponse> request)
{
var requestName = request.GetType().Name;
BackgroundJob.Enqueue<IMediator>(mediator => mediator.Send(requestName, request, CancellationToken.None));
return Task.CompletedTask;
}
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.Services.Extensions;
using Rsdo.Concordancer.Services.Services.ParagraphService;
namespace Rsdo.Concordancer.Services.RequestHandlers.Concordances;
public abstract class BaseSearchConcordancesHandler
{
private readonly IParagraphService paragraphService;
protected BaseSearchConcordancesHandler(IParagraphService paragraphService)
{
this.paragraphService = paragraphService;
}
protected async Task<List<SearchConcordancesResponseItem>> GetItems(ConcordancesQuery query, List<string> entityIds)
{
var items = new List<SearchConcordancesResponseItem>();
foreach (var entityId in entityIds)
{
items.Add(await GetItem(query, entityId));
}
return items;
}
private async Task<SearchConcordancesResponseItem> GetItem(ConcordancesQuery query, string entityId)
{
var centerTokenId = Guid.Parse(entityId);
var centerToken = await paragraphService.GetToken(centerTokenId);
var paragraphId = centerToken.Sentence.Paragraph.Id;
var orderStart = Math.Max(centerToken.TokenOrder - 20, 1);
var orderEnd = centerToken.TokenOrder + 20;
// Get predicate for tokens limitation
Expression<Func<Token, bool>> predicate = t =>
t.Sentence.Paragraph.Id == paragraphId && t.TokenOrder >= orderStart && t.TokenOrder <= orderEnd;
// Get tokens
var tokens = await paragraphService.GetTokens(predicate);
// Get center token index
var centerTokenIndex = tokens.FindIndex(t => t.TokenOrder == centerToken.TokenOrder);
// Highlight tokens
tokens.Highlight(query, centerTokenIndex);
// Return response item
return new SearchConcordancesResponseItem()
{
ParagraphId = paragraphId,
LeftContext = tokens.GetRange(0, centerTokenIndex),
CenterContext = tokens[centerTokenIndex],
RightContext = tokens.GetRange(centerTokenIndex + 1, tokens.Count - centerTokenIndex - 1),
};
}
}
@@ -0,0 +1,56 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.Services.Extensions;
using Rsdo.Concordancer.Services.Framework.DbContext;
using Rsdo.Concordancer.Services.Search.QueryFactories;
using Rsdo.Concordancer.Services.Services.ParagraphService;
namespace Rsdo.Concordancer.Services.RequestHandlers.Concordances;
public class ConcordanceDetailsHandler : IRequestHandler<ConcordanceDetails, ConcordanceDetailsResponse>
{
private readonly CorpusDbContext dbContext;
private readonly IQueryFactory<ConcordanceDetails, ConcordancesQuery> queryFactory;
private readonly IParagraphService paragraphService;
public ConcordanceDetailsHandler(
CorpusDbContext dbContext,
IQueryFactory<ConcordanceDetails, ConcordancesQuery> queryFactory,
IParagraphService paragraphService)
{
this.dbContext = dbContext;
this.queryFactory = queryFactory;
this.paragraphService = paragraphService;
}
public async Task<ConcordanceDetailsResponse> Handle(ConcordanceDetails request, CancellationToken cancellationToken)
{
// Get text
var text = await dbContext.Paragraph.Include(p => p.Text).Where(p => p.Id == request.ParagraphId).Select(x => x.Text).SingleAsync(cancellationToken);
// Get tokens in paragraph
var tokens = await paragraphService.GetTokens(x => x.Sentence.Paragraph.Id == request.ParagraphId);
// Highlight tokens
var centerTokenIndex = tokens.FindIndex(t => t.TokenOrder == request.TokenOrder);
if (centerTokenIndex != -1)
{
var query = await queryFactory.GetQuery(request);
tokens.Highlight(query, centerTokenIndex);
}
return new ConcordanceDetailsResponse()
{
Author = text.Author,
Title = text.Title,
Tokens = tokens,
Year = text.Year,
SourceFile = text.DisplayFileName,
};
}
}
@@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Constants;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Search.QueryFactories;
using Rsdo.Concordancer.Services.Services.ParagraphService;
namespace Rsdo.Concordancer.Services.RequestHandlers.Concordances;
public class ExportConcordancesHandler : BaseSearchConcordancesHandler, IRequestHandler<ExportConcordances, ExportConcordancesResponse>
{
private readonly IQueryFactory<ExportConcordances, ConcordancesQuery> queryFactory;
private readonly ISearchEngine searchEngine;
public ExportConcordancesHandler(
IParagraphService paragraphService,
IQueryFactory<ExportConcordances, ConcordancesQuery> queryFactory,
ISearchEngine searchEngine)
: base(paragraphService)
{
this.queryFactory = queryFactory;
this.searchEngine = searchEngine;
}
public async Task<ExportConcordancesResponse> Handle(ExportConcordances request, CancellationToken cancellationToken)
{
// Convert request to search query
var query = await queryFactory.GetQuery(request);
// Execute search
var result = await searchEngine.Search<Concordance, ConcordancesQuery>(query);
// Get search items
var items = await GetItems(query, result.EntityIds);
// Export
var stream = new MemoryStream();
await using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
// Writer header
await WriteHeader(writer);
// Write items
foreach (var item in items)
{
await WriteItem(writer, item);
}
}
stream.Position = 0;
return new ExportConcordancesResponse()
{
ContentType = Constants.Export.DefaultContentType,
FileName = "export.txt",
Stream = stream,
};
}
private static async Task WriteContext(StreamWriter writer, List<ConcordanceToken> tokens)
{
if (tokens != null)
{
foreach (var token in tokens)
{
switch (token.Type)
{
case TokenType.Word or TokenType.PunctuationCharacter:
await writer.WriteAsync(token.Form);
break;
case TokenType.Character:
await writer.WriteAsync(" ");
break;
}
}
}
}
private static async Task WriteHeader(StreamWriter writer)
{
await writer.WriteAsync("Levo besedilo");
await writer.WriteAsync("\t");
await writer.WriteAsync("Iskani niz");
await writer.WriteAsync("\t");
await writer.WriteAsync("Desno besedilo");
await writer.WriteLineAsync();
}
private static async Task WriteItem(StreamWriter writer, SearchConcordancesResponseItem item)
{
await WriteContext(writer, item.LeftContext);
await writer.WriteAsync("\t");
await writer.WriteAsync(item.CenterContext.Form);
await writer.WriteAsync("\t");
await WriteContext(writer, item.RightContext);
await writer.WriteLineAsync();
}
}
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Search.Aggregations;
using Rsdo.Concordancer.Services.Search.AlternateSearches;
using Rsdo.Concordancer.Services.Search.QueryFactories;
using Rsdo.Concordancer.Services.Services.ParagraphService;
namespace Rsdo.Concordancer.Services.RequestHandlers.Concordances;
public class SearchConcordancesHandler : BaseSearchConcordancesHandler, IRequestHandler<SearchConcordances, SearchConcordancesResponse>
{
private readonly IAggregationProviderFactory aggregationProviderFactory;
private readonly IAlternateSearchProvider<SearchConcordances, SearchConcordancesResponse> lemmasAlternateSearchProvider;
private readonly IQueryFactory<SearchConcordances, ConcordancesQuery> queryFactory;
private readonly ISearchEngine searchEngine;
public SearchConcordancesHandler(
IAggregationProviderFactory aggregationProviderFactory,
IAlternateSearchProvider<SearchConcordances, SearchConcordancesResponse> lemmasAlternateSearchProvider,
IParagraphService paragraphService,
IQueryFactory<SearchConcordances, ConcordancesQuery> queryFactory,
ISearchEngine searchEngine)
: base(paragraphService)
{
this.aggregationProviderFactory = aggregationProviderFactory;
this.lemmasAlternateSearchProvider = lemmasAlternateSearchProvider;
this.queryFactory = queryFactory;
this.searchEngine = searchEngine;
}
public async Task<SearchConcordancesResponse> Handle(SearchConcordances request, CancellationToken cancellationToken)
{
// Convert request to search query
var query = await queryFactory.GetQuery(request);
// Execute search
var result = await searchEngine.Search<Concordance, ConcordancesQuery>(query);
return new SearchConcordancesResponse()
{
Items = await GetItems(query, result.EntityIds),
Aggregations = new List<Aggregation>()
{
await GetAggregation(AggregationType.Text, c => c.TextIds?.Clear()),
},
LemmasAlternateSearch = await lemmasAlternateSearchProvider.Get(request),
Offset = request.From,
Total = result.Total,
};
Task<Aggregation> GetAggregation(AggregationType aggregationType, Action<ConcordancesQuery> modifyAction)
{
// Clone query
var clonedQuery = query.Copy();
modifyAction(clonedQuery);
// Get aggregation
var provider = aggregationProviderFactory.GetProvider(aggregationType);
return provider.Get(clonedQuery);
}
}
}
@@ -0,0 +1,48 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class CreateCorpusHandler : IRequestHandler<CreateCorpus, ExecutionResult>
{
private readonly MasterDbContext dbContext;
private readonly IServiceBus serviceBus;
public CreateCorpusHandler(MasterDbContext dbContext, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(CreateCorpus request, CancellationToken cancellationToken)
{
// Create new corpus and mark its status as Creating
var corpus = new Corpus
{
Description = request.Description,
Title = request.Title,
Status = CorpusStatus.Creating,
}.ApplyCreateValues();
await dbContext.Corpus.AddAsync(corpus, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// Send job to create corpus store
await serviceBus.Send(
new CreateCorpusStore
{
CorpusId = corpus.Id,
});
// Return result
return new ExecutionResult().WithEntityInfo(EntityType.Corpus, corpus.Id);
}
}
@@ -0,0 +1,57 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DatabaseManager;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class CreateCorpusStoreHandler : IRequestHandler<CreateCorpusStore, ExecutionResult>
{
private readonly IDatabaseManager databaseManager;
private readonly MasterDbContext dbContext;
private readonly ISearchEngine searchEngine;
public CreateCorpusStoreHandler(IDatabaseManager databaseManager, MasterDbContext dbContext, ISearchEngine searchEngine)
{
this.databaseManager = databaseManager;
this.dbContext = dbContext;
this.searchEngine = searchEngine;
}
public async Task<ExecutionResult> Handle(CreateCorpusStore request, CancellationToken cancellationToken)
{
// Get corpus
var corpus = await dbContext.Corpus.SingleAsync(x => x.Id == request.CorpusId, cancellationToken);
// Check corpus status
if (corpus.Status != CorpusStatus.Creating)
{
throw new XForbiddenException(Errors.Forbidden.CorpusStatusIsNotValid(corpus.Status, CorpusStatus.Creating));
}
// Create database for corpus
var createDbTask = databaseManager.CreateCorpusDatabase();
// Create elastic indexes
var createSchemaTask = searchEngine.CreateSchema();
// Wait to finish
await Task.WhenAll(createDbTask, createSchemaTask);
// Mark corpus as active
corpus.Status = CorpusStatus.Active;
await dbContext.SaveChangesAsync(cancellationToken);
// Return result
return new ExecutionResult();
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class CreateCorpusValidator : AbstractValidator<CreateCorpus>
{
public CreateCorpusValidator()
{
RuleFor(x => x.Title).NotEmpty();
}
}
@@ -0,0 +1,41 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class DeleteCorpusHandler : IRequestHandler<DeleteCorpus, ExecutionResult>
{
private readonly MasterDbContext dbContext;
private readonly IServiceBus serviceBus;
public DeleteCorpusHandler(MasterDbContext dbContext, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(DeleteCorpus request, CancellationToken cancellationToken)
{
// Get corpus
var corpus = await dbContext.Corpus.SingleAsync(x => x.Id == request.CorpusId, cancellationToken);
// Set status to deleted
corpus.Status = CorpusStatus.Deleted;
await dbContext.SaveChangesAsync(cancellationToken);
// Send job to create corpus store
await serviceBus.Send(
new DeleteCorpusStore
{
CorpusId = corpus.Id,
});
return new ExecutionResult();
}
}
@@ -0,0 +1,48 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.Services.Framework.DatabaseManager;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class DeleteCorpusStoreHandler : IRequestHandler<DeleteCorpusStore, ExecutionResult>
{
private readonly IDatabaseManager databaseManager;
private readonly MasterDbContext dbContext;
private readonly ISearchEngine searchEngine;
public DeleteCorpusStoreHandler(IDatabaseManager databaseManager, MasterDbContext dbContext, ISearchEngine searchEngine)
{
this.databaseManager = databaseManager;
this.dbContext = dbContext;
this.searchEngine = searchEngine;
}
public async Task<ExecutionResult> Handle(DeleteCorpusStore request, CancellationToken cancellationToken)
{
// Get corpus
var corpus = await dbContext.Corpus.SingleAsync(x => x.Id == request.CorpusId, cancellationToken);
// Create database for corpus
var deleteDbTask = databaseManager.DeleteCorpusDatabase();
// Create elastic indexes
var deleteSchemaTask = searchEngine.DeleteSchema();
// Wait to finish
await Task.WhenAll(deleteDbTask, deleteSchemaTask);
// Remove corpus
dbContext.Corpus.Remove(corpus);
await dbContext.SaveChangesAsync(cancellationToken);
// Return result
return new ExecutionResult();
}
}
@@ -0,0 +1,32 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class GetCorpusHandler : IRequestHandler<GetCorpus, GetCorpusResponse>
{
private readonly MasterDbContext dbContext;
public GetCorpusHandler(MasterDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<GetCorpusResponse> Handle(GetCorpus request, CancellationToken cancellationToken)
{
// Get corpus
var corpus = await dbContext.Corpus.SingleAsync(x => x.Id == request.CorpusId, cancellationToken);
return new GetCorpusResponse()
{
Description = corpus.Description,
Id = corpus.Id,
Status = corpus.Status,
Title = corpus.Title,
};
}
}
@@ -0,0 +1,33 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Corpuses;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Corpuses;
public class GetCorpusStatisticsHandler : IRequestHandler<GetCorpusStatistics, GetCorpusStatisticsResponse>
{
private readonly CorpusDbContext dbContext;
public GetCorpusStatisticsHandler(CorpusDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<GetCorpusStatisticsResponse> Handle(GetCorpusStatistics request, CancellationToken cancellationToken)
{
var texts = await dbContext.Text.CountAsync(cancellationToken);
var sentences = await dbContext.Sentence.CountAsync(cancellationToken);
var words = await dbContext.Token.CountAsync(t => t.Type == TokenType.Word, cancellationToken);
return new GetCorpusStatisticsResponse()
{
Texts = texts,
Sentences = sentences,
Words = words,
};
}
}
@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Sloleks;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.Services.Framework.BulkLoaders;
namespace Rsdo.Concordancer.Services.RequestHandlers.Sloleks;
public class ImportSloleksHandler : IRequestHandler<ImportSloleks, ExecutionResult>
{
private readonly IBulkLoader bulkLoader;
public ImportSloleksHandler(IBulkLoader bulkLoader)
{
this.bulkLoader = bulkLoader;
}
public async Task<ExecutionResult> Handle(ImportSloleks request, CancellationToken cancellationToken)
{
var sourceFile = request.SourceFile;
if (!File.Exists(sourceFile))
{
throw new FileNotFoundException($"File {sourceFile} not found!", sourceFile);
}
var items = Import(sourceFile);
await SaveData(items, cancellationToken);
return new ExecutionResult();
}
private static string GetFeature(XElement element, string featName)
{
return element?.Elements("feat").SingleOrDefault(e => e.Attribute("att")?.Value == featName)?.Attribute("val")?.Value;
}
private static Dictionary<string, HashSet<string>> Import(string sourceFile)
{
var items = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
using var stream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
using var xmlReader = XmlReader.Create(stream);
while (xmlReader.Read())
{
if (xmlReader.NodeType != XmlNodeType.Element || xmlReader.LocalName != "LexicalEntry")
{
continue;
}
using var entryReader = xmlReader.ReadSubtree();
ImportEntry(entryReader, items);
}
return items;
}
private static void ImportEntry(XmlReader entryReader, Dictionary<string, HashSet<string>> items)
{
var entryEl = XElement.Load(entryReader);
// Skip if multiword
var partOfSpeech = GetFeature(entryEl, "besedna_vrsta");
if (!string.IsNullOrEmpty(partOfSpeech) && partOfSpeech == "večbesedna_enota")
{
return;
}
// Get lemma
var lemmaEl = entryEl.Element("Lemma");
var lemma = GetFeature(lemmaEl, "zapis_oblike");
// Add lemma to items
if (!items.ContainsKey(lemma))
{
items.Add(lemma, new HashSet<string>(StringComparer.Ordinal));
}
// Get forms
foreach (var wordFormEl in entryEl.Elements("WordForm"))
{
foreach (var representationEl in wordFormEl.Elements("FormRepresentation"))
{
var form = GetFeature(representationEl, "zapis_oblike");
if (string.IsNullOrEmpty(form))
{
continue;
}
if (!items[lemma].Contains(form))
{
items[lemma].Add(form);
}
}
}
}
private async Task SaveData(Dictionary<string, HashSet<string>> items, CancellationToken cancellationToken)
{
var pairs = from lemma in items.Keys
from form in items[lemma]
select new LemmaFormPair()
{
Lemma = lemma,
Form = form,
}.ApplyCreateValues();
foreach (var batch in pairs.Chunk(50000))
{
await bulkLoader.InsertEntities(batch.ToList(), cancellationToken);
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public abstract class BaseSearchTermListHandler
{
private readonly CorpusDbContext dbContext;
protected BaseSearchTermListHandler(CorpusDbContext dbContext)
{
this.dbContext = dbContext;
}
protected async Task<List<SearchTermListResponseItem>> GetItems(List<string> entityIds)
{
var ids = entityIds.Select(Guid.Parse).ToList();
var terms = await dbContext.Term.Where(t => ids.Contains(t.Id)).ToListAsync();
return (from entityId in entityIds
let termId = Guid.Parse(entityId)
join term in terms on termId equals term.Id
select new SearchTermListResponseItem()
{
Form = term.Form,
Frequency = term.Frequency,
Lemma = term.Lemma,
Weight = term.Weight,
}).ToList();
}
}
@@ -0,0 +1,47 @@
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class CreateTermListHandler : IRequestHandler<CreateTermList, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly IServiceBus serviceBus;
public CreateTermListHandler(CorpusDbContext dbContext, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(CreateTermList request, CancellationToken cancellationToken)
{
// Create new term list
var termList = new TermList()
{
SourceFile = request.SourceFile,
Status = ImportStatus.Waiting,
}.ApplyCreateValues();
await dbContext.TermList.AddAsync(termList, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// Send job to import term list
await serviceBus.Send(
new ImportTermList()
{
CorpusId = request.CorpusId,
TermListId = termList.Id,
});
// Return result
return new ExecutionResult().WithEntityInfo(EntityType.TermList, termList.Id);
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class CreateTermListValidator : AbstractValidator<CreateTermList>
{
public CreateTermListValidator()
{
RuleFor(x => x.SourceFile).NotEmpty();
}
}
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class DeleteTermListFromIndexHandler : IRequestHandler<DeleteTermListFromIndex, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly ISearchEngine searchEngine;
private readonly IServiceBus serviceBus;
public DeleteTermListFromIndexHandler(CorpusDbContext dbContext, ISearchEngine searchEngine, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.searchEngine = searchEngine;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(DeleteTermListFromIndex request, CancellationToken cancellationToken)
{
// Get term list
var termList = await dbContext.TermList.SingleOrDefaultAsync(t => t.Id == request.TermListId, cancellationToken);
if (termList == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.TermList, request.TermListId));
}
// Loop through terms in batches and delete terms from index
int skip = 0;
List<Guid> entityIds;
while ((entityIds = await dbContext.Term.Where(t => t.TermListAutoId == termList.AutoId)
.OrderBy(t => t.AutoId)
.Skip(skip)
.Take(1000)
.Select(t => t.Id)
.ToListAsync(cancellationToken)).Any())
{
await searchEngine.Delete<Term>(entityIds);
skip += entityIds.Count;
}
// Commit changes in index
await searchEngine.Commit();
// Send job to delete term list from store
await serviceBus.Send(
new DeleteTermListFromStore()
{
CorpusId = request.CorpusId,
TermListId = termList.Id,
});
return new ExecutionResult();
}
}
@@ -0,0 +1,39 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class DeleteTermListFromStoreHandler : IRequestHandler<DeleteTermListFromStore, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
public DeleteTermListFromStoreHandler(CorpusDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<ExecutionResult> Handle(DeleteTermListFromStore request, CancellationToken cancellationToken)
{
// Get term list
var termList = await dbContext.TermList.SingleOrDefaultAsync(t => t.Id == request.TermListId, cancellationToken);
if (termList == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.TermList, request.TermListId));
}
// Delete term list and all depending objects
dbContext.TermList.Remove(termList);
// Save changes to database
await dbContext.SaveChangesAsync(cancellationToken);
return new ExecutionResult();
}
}
@@ -0,0 +1,47 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class DeleteTermListHandler : IRequestHandler<DeleteTermList, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly IServiceBus serviceBus;
public DeleteTermListHandler(CorpusDbContext dbContext, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(DeleteTermList request, CancellationToken cancellationToken)
{
// Get term list
var termList = await dbContext.TermList.SingleOrDefaultAsync(t => t.Id == request.TermListId, cancellationToken);
if (termList == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.TermList, request.TermListId));
}
// Mark term list as deleted
termList.Status = ImportStatus.Deleted;
await dbContext.SaveChangesAsync(cancellationToken);
// Send job to delete term list from index
await serviceBus.Send(
new DeleteTermListFromIndex()
{
CorpusId = request.CorpusId,
TermListId = termList.Id,
});
return new ExecutionResult();
}
}
@@ -0,0 +1,81 @@
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Constants;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Queries.TermLists;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.Services.Framework.DbContext;
using Rsdo.Concordancer.Services.Search.QueryFactories;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class ExportTermListHandler : BaseSearchTermListHandler, IRequestHandler<ExportTermList, ExportTermListResponse>
{
private readonly IQueryFactory<ExportTermList, TermListQuery> queryFactory;
private readonly ISearchEngine searchEngine;
public ExportTermListHandler(CorpusDbContext dbContext, IQueryFactory<ExportTermList, TermListQuery> queryFactory, ISearchEngine searchEngine)
: base(dbContext)
{
this.queryFactory = queryFactory;
this.searchEngine = searchEngine;
}
public async Task<ExportTermListResponse> Handle(ExportTermList request, CancellationToken cancellationToken)
{
// Convert request to search query
var query = await queryFactory.GetQuery(request);
// Execute search
var result = await searchEngine.Search<Term, TermListQuery>(query);
// Get search items
var items = await GetItems(result.EntityIds);
// Export
var stream = new MemoryStream();
await using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
{
// Writer header
await WriteHeader(writer);
// Write items
foreach (var item in items)
{
await WriteItem(writer, item);
}
}
stream.Position = 0;
return new ExportTermListResponse()
{
ContentType = Constants.Export.DefaultContentType,
FileName = "export.txt",
Stream = stream,
};
}
private static async Task WriteHeader(StreamWriter writer)
{
await writer.WriteAsync("Termin");
await writer.WriteAsync("\t");
await writer.WriteAsync("Osnovna oblika");
await writer.WriteAsync("\t");
await writer.WriteAsync("Število pojavitev");
await writer.WriteLineAsync();
}
private static async Task WriteItem(StreamWriter writer, SearchTermListResponseItem item)
{
await writer.WriteAsync(item.Form);
await writer.WriteAsync("\t");
await writer.WriteAsync(item.Lemma);
await writer.WriteAsync("\t");
await writer.WriteAsync(item.Frequency.ToString());
await writer.WriteLineAsync();
}
}
@@ -0,0 +1,37 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class GetTermListHandler : IRequestHandler<GetTermList, GetTermListResponse>
{
private readonly CorpusDbContext dbContext;
public GetTermListHandler(CorpusDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<GetTermListResponse> Handle(GetTermList request, CancellationToken cancellationToken)
{
// Get term list
var termList = await dbContext.TermList.SingleOrDefaultAsync(t => t.Id == request.TermListId, cancellationToken);
if (termList == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.TermList, request.TermListId));
}
return new GetTermListResponse()
{
Id = termList.Id,
SourceFile = termList.SourceFile,
Status = termList.Status,
};
}
}
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.BulkLoaders;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class ImportTermListHandler : IRequestHandler<ImportTermList, ExecutionResult>
{
private readonly IBulkLoader bulkLoader;
private readonly CorpusDbContext dbContext;
private readonly IServiceBus serviceBus;
public ImportTermListHandler(IBulkLoader bulkLoader, CorpusDbContext dbContext, IServiceBus serviceBus)
{
this.bulkLoader = bulkLoader;
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(ImportTermList request, CancellationToken cancellationToken)
{
// Get term list
// Dont track term list since the data is imported with bulk loading and that would cause duplicate entities to be inserted
var termList = await dbContext.TermList.AsNoTracking().SingleOrDefaultAsync(t => t.Id == request.TermListId, cancellationToken);
if (termList == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.TermList, request.TermListId));
}
// Check if file exists
if (!File.Exists(termList.SourceFile))
{
throw new FileNotFoundException(Errors.NotFound.FileNotFound(termList.SourceFile), termList.SourceFile);
}
try
{
// Change status to importing
await ChangeStatus(termList, ImportStatus.Importing, cancellationToken);
// Import term list
await ImportTermList(termList, cancellationToken);
// Bulk load data into database
await SaveTermListWithBulkLoading(termList, cancellationToken);
// Send job to index term list
await serviceBus.Send(
new IndexTermList()
{
CorpusId = request.CorpusId,
TermListId = termList.Id,
});
// Change status to importing completed
await ChangeStatus(termList, ImportStatus.ImportingCompleted, cancellationToken);
return new ExecutionResult();
}
catch (Exception)
{
await ChangeStatus(termList, ImportStatus.ImportingFaulted, cancellationToken);
throw;
}
}
private static async Task ImportTermList(TermList termList, CancellationToken cancellationToken)
{
var lastPropertyName = string.Empty;
var inTerms = false;
await using var stream = new FileStream(termList.SourceFile, FileMode.Open, FileAccess.Read);
using var streamReader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(streamReader);
while (await jsonReader.ReadAsync(cancellationToken))
{
switch (jsonReader.TokenType)
{
case JsonToken.PropertyName:
lastPropertyName = jsonReader.Value.ToString();
break;
case JsonToken.StartArray when lastPropertyName == "terminoloski_kandidati":
inTerms = true;
break;
case JsonToken.EndArray when inTerms:
inTerms = false;
break;
case JsonToken.StartObject when inTerms:
{
var termObj = await JObject.LoadAsync(jsonReader, cancellationToken);
var term = new Term()
{
Form = termObj["kanonicnaoblika"].Value<string>(),
Lemma = termObj["kandidat"].Value<string>(),
Msd = termObj["POSoznake"].Value<string>(),
Weight = termObj["ranking"].Value<decimal>(),
Frequency = termObj["pogostostpojavljanja"].Value<int>(),
TermListAutoId = termList.AutoId,
}.ApplyCreateValues();
termList.Terms ??= new List<Term>();
termList.Terms.Add(term);
break;
}
}
}
}
private async Task SaveTermListWithBulkLoading(TermList termList, CancellationToken cancellationToken)
{
if (termList.Terms.IsNullOrEmpty())
{
return;
}
await bulkLoader.InsertEntities(termList.Terms, false, cancellationToken);
}
private async Task ChangeStatus(TermList termList, ImportStatus newStatus, CancellationToken cancellationToken)
{
termList.Status = newStatus;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class IndexTermListHandler : IRequestHandler<IndexTermList, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly ISearchEngine searchEngine;
public IndexTermListHandler(CorpusDbContext dbContext, ISearchEngine searchEngine)
{
this.dbContext = dbContext;
this.searchEngine = searchEngine;
}
public async Task<ExecutionResult> Handle(IndexTermList request, CancellationToken cancellationToken)
{
// Get term list
var termList = await dbContext.TermList.SingleOrDefaultAsync(t => t.Id == request.TermListId, cancellationToken);
if (termList == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.TermList, request.TermListId));
}
try
{
// Change status to indexing
await ChangeStatus(termList, ImportStatus.Indexing, cancellationToken);
// Index term list
await IndexTermList(termList, cancellationToken);
// Change status to indexing completed and active
await ChangeStatus(termList, ImportStatus.IndexingCompleted, cancellationToken);
await ChangeStatus(termList, ImportStatus.Active, cancellationToken);
return new ExecutionResult();
}
catch (Exception)
{
await ChangeStatus(termList, ImportStatus.IndexingFaulted, cancellationToken);
throw;
}
}
private async Task IndexTermList(TermList termList, CancellationToken cancellationToken)
{
// Loop through terms
List<Term> terms;
var lastAutoId = 0L;
while ((terms = await GetNextTerms(termList, lastAutoId, cancellationToken)).Any())
{
// Index terms
await searchEngine.Add(terms);
lastAutoId = terms.Max(t => t.AutoId);
}
// Commit indexed terms
await searchEngine.Commit();
}
private async Task<List<Term>> GetNextTerms(TermList termList, long lastAutoId, CancellationToken cancellationToken)
{
return await dbContext.Term.Where(t => t.TermListAutoId == termList.AutoId && t.AutoId > lastAutoId)
.OrderBy(t => t.AutoId)
.Take(500)
.ToListAsync(cancellationToken);
}
private async Task ChangeStatus(TermList termList, ImportStatus newStatus, CancellationToken cancellationToken)
{
termList.Status = newStatus;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
@@ -0,0 +1,40 @@
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Queries.TermLists;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.Services.Framework.DbContext;
using Rsdo.Concordancer.Services.Search.QueryFactories;
namespace Rsdo.Concordancer.Services.RequestHandlers.TermLists;
public class SearchTermListHandler : BaseSearchTermListHandler, IRequestHandler<SearchTermList, SearchTermListResponse>
{
private readonly IQueryFactory<SearchTermList, TermListQuery> queryFactory;
private readonly ISearchEngine searchEngine;
public SearchTermListHandler(CorpusDbContext dbContext, IQueryFactory<SearchTermList, TermListQuery> queryFactory, ISearchEngine searchEngine)
: base(dbContext)
{
this.queryFactory = queryFactory;
this.searchEngine = searchEngine;
}
public async Task<SearchTermListResponse> Handle(SearchTermList request, CancellationToken cancellationToken)
{
// Convert request to search query
var query = await queryFactory.GetQuery(request);
// Execute search
var result = await searchEngine.Search<Term, TermListQuery>(query);
return new SearchTermListResponse()
{
Items = await GetItems(result.EntityIds),
Offset = request.From,
Total = result.Total,
};
}
}
@@ -0,0 +1,50 @@
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class CreateTextHandler : IRequestHandler<CreateText, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly IServiceBus serviceBus;
public CreateTextHandler(CorpusDbContext dbContext, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(CreateText request, CancellationToken cancellationToken)
{
// Create new text
var text = new Text()
{
Author = request.Author,
SourceFile = request.SourceFile,
Status = ImportStatus.Waiting,
Title = request.Title,
Year = request.Year,
}.ApplyCreateValues();
await dbContext.Text.AddAsync(text, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
// Send job to import text
await serviceBus.Send(
new ImportText()
{
CorpusId = request.CorpusId,
TextId = text.Id,
});
// Return result
return new ExecutionResult().WithEntityInfo(EntityType.Text, text.Id);
}
}
@@ -0,0 +1,12 @@
using FluentValidation;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class CreateTextValidator : AbstractValidator<CreateText>
{
public CreateTextValidator()
{
RuleFor(x => x.SourceFile).NotEmpty();
}
}
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class DeleteTextFromIndexHandler : IRequestHandler<DeleteTextFromIndex, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly ISearchEngine searchEngine;
private readonly IServiceBus serviceBus;
public DeleteTextFromIndexHandler(CorpusDbContext dbContext, ISearchEngine searchEngine, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.searchEngine = searchEngine;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(DeleteTextFromIndex request, CancellationToken cancellationToken)
{
// Get text
var text = await dbContext.Text.SingleOrDefaultAsync(t => t.Id == request.TextId, cancellationToken);
if (text == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Text, request.TextId));
}
// Loop through tokens in batches and delete concordances from index
int skip = 0;
List<Guid> entityIds;
while ((entityIds = await dbContext.Token.Include(t => t.Sentence)
.ThenInclude(t => t.Paragraph)
.Where(t => t.Sentence.Paragraph.TextAutoId == text.AutoId && t.Type != TokenType.Character)
.OrderBy(t => t.AutoId)
.Skip(skip)
.Take(1000)
.Select(t => t.Id)
.ToListAsync(cancellationToken)).Any())
{
await searchEngine.Delete<Concordance>(entityIds);
skip += entityIds.Count;
}
// Commit changes in index
await searchEngine.Commit();
// Send job to delete text from store
await serviceBus.Send(
new DeleteTextFromStore()
{
CorpusId = request.CorpusId,
TextId = text.Id,
});
return new ExecutionResult();
}
}
@@ -0,0 +1,39 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class DeleteTextFromStoreHandler : IRequestHandler<DeleteTextFromStore, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
public DeleteTextFromStoreHandler(CorpusDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<ExecutionResult> Handle(DeleteTextFromStore request, CancellationToken cancellationToken)
{
// Get text
var text = await dbContext.Text.SingleOrDefaultAsync(t => t.Id == request.TextId, cancellationToken);
if (text == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Text, request.TextId));
}
// Delete text and all depending objects
dbContext.Text.Remove(text);
// Save changes to database
await dbContext.SaveChangesAsync(cancellationToken);
return new ExecutionResult();
}
}
@@ -0,0 +1,47 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class DeleteTextHandler : IRequestHandler<DeleteText, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly IServiceBus serviceBus;
public DeleteTextHandler(CorpusDbContext dbContext, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(DeleteText request, CancellationToken cancellationToken)
{
// Get text
var text = await dbContext.Text.SingleOrDefaultAsync(t => t.Id == request.TextId, cancellationToken);
if (text == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Text, request.TextId));
}
// Mark text as deleted
text.Status = ImportStatus.Deleted;
await dbContext.SaveChangesAsync(cancellationToken);
// Send job to delete text from index
await serviceBus.Send(
new DeleteTextFromIndex()
{
CorpusId = request.CorpusId,
TextId = text.Id,
});
return new ExecutionResult();
}
}
@@ -0,0 +1,40 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class GetTextHandler : IRequestHandler<GetText, GetTextResponse>
{
private readonly CorpusDbContext dbContext;
public GetTextHandler(CorpusDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<GetTextResponse> Handle(GetText request, CancellationToken cancellationToken)
{
// Get text
var text = await dbContext.Text.SingleOrDefaultAsync(t => t.Id == request.TextId, cancellationToken);
if (text == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Text, request.TextId));
}
return new GetTextResponse()
{
Author = text.Author,
Id = text.Id,
SourceFile = text.SourceFile,
Status = text.Status,
Title = text.Title,
Year = text.Year,
};
}
}
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.BulkLoaders;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class ImportTextHandler : IRequestHandler<ImportText, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly IBulkLoader bulkLoader;
private readonly IServiceBus serviceBus;
public ImportTextHandler(CorpusDbContext dbContext, IBulkLoader bulkLoader, IServiceBus serviceBus)
{
this.dbContext = dbContext;
this.bulkLoader = bulkLoader;
this.serviceBus = serviceBus;
}
public async Task<ExecutionResult> Handle(ImportText request, CancellationToken cancellationToken)
{
// Get text
// Dont track text since the data is imported with bulk loading and that would cause duplicate entities to be inserted
var text = await dbContext.Text.AsNoTracking().SingleOrDefaultAsync(t => t.Id == request.TextId, cancellationToken);
if (text == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Text, request.TextId));
}
// Check if file exists
if (!File.Exists(text.SourceFile))
{
throw new FileNotFoundException(Errors.NotFound.FileNotFound(text.SourceFile), text.SourceFile);
}
try
{
// Change status to importing
await ChangeStatus(text, ImportStatus.Importing, cancellationToken);
// Import text
await ImportText(text);
// Bulk load data into database
await SaveTextWithBulkLoading(text, cancellationToken);
// Send job to index text
await serviceBus.Send(
new IndexText()
{
CorpusId = request.CorpusId,
TextId = text.Id,
});
// Change status to importing completed
await ChangeStatus(text, ImportStatus.ImportingCompleted, cancellationToken);
return new ExecutionResult();
}
catch (Exception)
{
await ChangeStatus(text, ImportStatus.ImportingFaulted, cancellationToken);
throw;
}
}
private static async Task ImportText(Text text)
{
await using var stream = new FileStream(text.SourceFile, FileMode.Open, FileAccess.Read);
using (var streamReader = new StreamReader(stream))
{
string line;
var paragraphOrder = 0;
var sentenceOrder = 0;
var tokenOrder = 0;
var paragraphTokenOrder = 0;
Paragraph paragraph = null;
Sentence sentence = null;
while ((line = await streamReader.ReadLineAsync()) != null)
{
if (line.StartsWith("# newpar", StringComparison.OrdinalIgnoreCase))
{
// New paragraph
paragraph = new Paragraph()
{
RecordOrder = ++paragraphOrder,
TextAutoId = text.AutoId,
}.ApplyCreateValues();
text.Paragraphs ??= new List<Paragraph>();
text.Paragraphs.Add(paragraph);
// Reset sentence order and order of tokens in paragraph
sentenceOrder = 0;
paragraphTokenOrder = 0;
}
else if (line.StartsWith("# sent", StringComparison.OrdinalIgnoreCase))
{
// New sentence
sentence = new Sentence()
{
RecordOrder = ++sentenceOrder,
}.ApplyCreateValues();
paragraph.Sentences ??= new List<Sentence>();
paragraph.Sentences.Add(sentence);
// Reset token order
tokenOrder = 0;
}
else if (!line.StartsWith("#") && !string.IsNullOrWhiteSpace(line))
{
// New token
var data = line.Split('\t');
var token = new Token()
{
RecordOrder = ++tokenOrder,
TokenOrder = ++paragraphTokenOrder,
Form = data[1],
Lemma = data[2],
Type = data[3].Equals("PUNCT", StringComparison.OrdinalIgnoreCase) ? TokenType.PunctuationCharacter : TokenType.Word,
Msd = data[4],
}.ApplyCreateValues();
sentence.Tokens ??= new List<Token>();
sentence.Tokens.Add(token);
// Check if there should be a space after token
if (!data[9].Contains("SpaceAfter=No", StringComparison.OrdinalIgnoreCase))
{
token = new Token()
{
RecordOrder = ++tokenOrder,
TokenOrder = ++paragraphTokenOrder,
Form = " ",
Type = TokenType.Character,
}.ApplyCreateValues();
sentence.Tokens.Add(token);
}
}
}
}
}
private async Task SaveTextWithBulkLoading(Text text, CancellationToken cancellationToken)
{
var paragraphs = text.Paragraphs;
await bulkLoader.InsertEntities(paragraphs, cancellationToken);
paragraphs.ForEach(p => p.Sentences.ForEach(s => s.ParagraphAutoId = p.AutoId));
var sentences = paragraphs.SelectMany(p => p.Sentences).ToList();
await bulkLoader.InsertEntities(sentences, cancellationToken);
sentences.ForEach(s => s.Tokens.ForEach(t => t.SentenceAutoId = s.AutoId));
var tokens = sentences.SelectMany(s => s.Tokens).ToList();
await bulkLoader.InsertEntities(tokens, false, cancellationToken);
}
private async Task ChangeStatus(Text text, ImportStatus newStatus, CancellationToken cancellationToken)
{
text.Status = newStatus;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.ServiceModel.Requests.Texts;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.RequestHandlers.Texts;
public class IndexTextHandler : IRequestHandler<IndexText, ExecutionResult>
{
private readonly CorpusDbContext dbContext;
private readonly ISearchEngine searchEngine;
public IndexTextHandler(CorpusDbContext dbContext, ISearchEngine searchEngine)
{
this.dbContext = dbContext;
this.searchEngine = searchEngine;
}
public async Task<ExecutionResult> Handle(IndexText request, CancellationToken cancellationToken)
{
// Get text
var text = await dbContext.Text.SingleOrDefaultAsync(t => t.Id == request.TextId, cancellationToken);
if (text == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Text, request.TextId));
}
try
{
// Change status to indexing
await ChangeStatus(text, ImportStatus.Indexing, cancellationToken);
// Index text
await IndexText(text, cancellationToken);
// Change status to indexing completed and active
await ChangeStatus(text, ImportStatus.IndexingCompleted, cancellationToken);
await ChangeStatus(text, ImportStatus.Active, cancellationToken);
return new ExecutionResult();
}
catch (Exception)
{
await ChangeStatus(text, ImportStatus.IndexingFaulted, cancellationToken);
throw;
}
}
public async Task IndexText(Text text, CancellationToken cancellationToken)
{
// Get paragraphs
var paragraphs = await dbContext.Paragraph.Where(p => p.TextAutoId == text.AutoId).OrderBy(p => p.RecordOrder).ToListAsync(cancellationToken);
// Index each paragraph individually
foreach (var paragraph in paragraphs)
{
await IndexParagraph(text, paragraph, cancellationToken);
}
// Commit indexed concordances
await searchEngine.Commit();
}
public async Task IndexParagraph(Text text, Paragraph paragraph, CancellationToken cancellationToken)
{
// Get tokens (skip spaces)
var tokens = await dbContext.Token.Include(t => t.Sentence)
.Where(t => t.Sentence.ParagraphAutoId == paragraph.AutoId && t.Type != TokenType.Character)
.OrderBy(t => t.TokenOrder)
.ToListAsync(cancellationToken);
// Get concordances
var concordances = GetConcordances(text, paragraph, tokens);
// Add concordances to search engine
await searchEngine.Add(concordances);
}
private static List<Concordance> GetConcordances(Text text, Paragraph paragraph, List<Token> tokens)
{
// Get window size (max 10)
var windowSize = Math.Min(tokens.Count - 1, 10);
// Create list of tokens with positions
var tokensIdx = tokens.Select((t, i) => new KeyValuePair<int, Token>(i, t)).ToList();
// Get tokens which will appear in current concordance
List<KeyValuePair<int, Token>> concordanceTokens;
var concordances = new List<Concordance>();
while ((concordanceTokens = tokensIdx.Where(x => x.Key >= -windowSize && x.Key <= windowSize).ToList().OrderBy(x => x.Key).ToList()).Any())
{
if (concordanceTokens.Last().Key < 0)
{
break;
}
var concordance = new Concordance()
{
ParagraphId = paragraph.Id,
TextId = text.Id,
};
// Loop through tokens and set it in the position
foreach (var concordanceToken in concordanceTokens)
{
concordance.SetToken(concordanceToken.Value, concordanceToken.Key);
}
concordances.Add(concordance);
// Decrease indexes of tokens (shift tokens to left, relative to window size)
tokensIdx = tokensIdx.Select(x => new KeyValuePair<int, Token>(x.Key - 1, x.Value)).ToList();
}
return concordances;
}
private async Task ChangeStatus(Text text, ImportStatus newStatus, CancellationToken cancellationToken)
{
text.Status = newStatus;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<Version>1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="FluentValidation" Version="11.2.2" />
<PackageReference Include="Hangfire.Core" Version="1.7.31" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Npgsql" Version="6.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Rsdo.StyleCop" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Rsdo.Concordancer.Core\Rsdo.Concordancer.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,19 @@
using Autofac;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Search.Aggregations;
public class AggregationProviderFactory : IAggregationProviderFactory
{
private readonly ILifetimeScope lifetimeScope;
public AggregationProviderFactory(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public IAggregationProvider GetProvider(AggregationType aggregationType)
{
return lifetimeScope.ResolveKeyed<IAggregationProvider>(aggregationType);
}
}
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Aggregations;
using Rsdo.Concordancer.Core.Search.Queries;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Search.Aggregations;
public abstract class BaseAggregationProvider : IAggregationProvider
{
private readonly IAggregatorFactory aggregatorFactory;
protected BaseAggregationProvider(IAggregatorFactory aggregatorFactory)
{
this.aggregatorFactory = aggregatorFactory;
}
public abstract AggregationType Type { get; }
public async Task<Aggregation> Get<TQuery>(TQuery query)
where TQuery : Query
{
// Run aggregator
var aggregator = aggregatorFactory.GetAggregator(Type);
var items = await aggregator.Get(query);
return new Aggregation()
{
Items = await GetItems(items),
Type = Type,
};
}
protected abstract Task<List<AggregationItem>> GetItems(IDictionary<string, long> items);
}
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Search.Aggregations;
public interface IAggregationProvider
{
Task<Aggregation> Get<TQuery>(TQuery query)
where TQuery : Query;
}
@@ -0,0 +1,8 @@
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Search.Aggregations;
public interface IAggregationProviderFactory
{
IAggregationProvider GetProvider(AggregationType aggregationType);
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Search.Aggregations;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Search.Aggregations;
public class TextAggregationProvider : BaseAggregationProvider
{
private readonly CorpusDbContext dbContext;
public TextAggregationProvider(IAggregatorFactory aggregatorFactory, CorpusDbContext dbContext)
: base(aggregatorFactory)
{
this.dbContext = dbContext;
}
public override AggregationType Type => AggregationType.Text;
protected override async Task<List<AggregationItem>> GetItems(IDictionary<string, long> items)
{
var ids = items.Select(i => Guid.Parse(i.Key)).ToList();
var texts = await dbContext.Text.Where(t => ids.Contains(t.Id)).ToListAsync();
return (from item in items
let id = Guid.Parse(item.Key)
join text in texts on id equals text.Id
orderby item.Value descending, text.SourceFile
select new AggregationItem
{
Count = item.Value,
Key = text.Id,
Title = text.DisplayFileName,
}).ToList();
}
}
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.ServiceModel.Requests.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Search.AlternateSearches;
public interface IAlternateSearchProvider<TRequest, TResponse>
where TRequest : Search<TResponse>
{
Task<AlternateSearch<TRequest, TResponse>> Get(TRequest request);
}
@@ -0,0 +1,154 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Search.QueryFactories;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.AlternateSearches;
public class LemmasAlternateSearchProvider : IAlternateSearchProvider<SearchConcordances, SearchConcordancesResponse>
{
private readonly ILemmatizationService lemmatizationService;
private readonly IQueryFactory<SearchConcordances, ConcordancesQuery> queryFactory;
private readonly ISearchEngine searchEngine;
public LemmasAlternateSearchProvider(
ILemmatizationService lemmatizationService,
IQueryFactory<SearchConcordances, ConcordancesQuery> queryFactory,
ISearchEngine searchEngine)
{
this.lemmatizationService = lemmatizationService;
this.queryFactory = queryFactory;
this.searchEngine = searchEngine;
}
public async Task<AlternateSearch<SearchConcordances, SearchConcordancesResponse>> Get(SearchConcordances request)
{
// Main word
var lemmas = new List<List<string>>
{
await GetLemmasForWord(request.MainWord),
};
// Words in context
if (!request.WordsInContext.IsNullOrEmpty())
{
foreach (var wordInContext in request.WordsInContext)
{
if (SkipWord(wordInContext))
{
continue;
}
lemmas.Add(await GetLemmasForWord(wordInContext));
}
}
var products = lemmas.CartesianProduct();
var alternateSearch = new AlternateSearch<SearchConcordances, SearchConcordancesResponse>()
{
Items = new List<AlternateSearchItem<SearchConcordances, SearchConcordancesResponse>>(),
OriginalSearch = GetOriginalSearch(request),
Type = AlternateSearchType.Lemmas,
};
foreach (var product in products)
{
var productLemmas = product.ToList();
var clone = request.Copy();
// Main word
clone.MainWord.Lemma = productLemmas[0];
// Words in context
if (!clone.WordsInContext.IsNullOrEmpty())
{
for (var i = 0; i < clone.WordsInContext.Count; i++)
{
var wordInContext = clone.WordsInContext[i];
if (SkipWord(wordInContext))
{
continue;
}
wordInContext.Lemma = productLemmas[i + 1];
}
}
// Count results
var count = await GetCount(clone);
if (count > 0)
{
alternateSearch.Items.Add(
new AlternateSearchItem<SearchConcordances, SearchConcordancesResponse>()
{
Count = count,
Search = clone,
Title = GetTitle(clone),
});
}
}
return alternateSearch;
}
private static SearchConcordances GetOriginalSearch(SearchConcordances search)
{
var original = search.Copy();
original.MainWord.Lemma = null;
if (!original.WordsInContext.IsNullOrEmpty())
{
foreach (var wordInContext in original.WordsInContext)
{
wordInContext.Lemma = null;
}
}
return original;
}
private static string GetTitle(SearchConcordances search)
{
var sb = new StringBuilder();
sb.Append(search.MainWord.Lemma);
if (!search.WordsInContext.IsNullOrEmpty())
{
foreach (var wordInContext in search.WordsInContext)
{
sb.Append(' ');
sb.Append(wordInContext.Lemma);
}
}
return sb.ToString();
}
private static bool SkipWord(SearchedWordInContext wordInContext)
{
return wordInContext.ConditionType == ConditionType.IsNot || (string.IsNullOrEmpty(wordInContext.Form) && string.IsNullOrEmpty(wordInContext.Lemma));
}
private async Task<long> GetCount(SearchConcordances search)
{
var query = await queryFactory.GetQuery(search);
var results = await searchEngine.Search<Concordance, ConcordancesQuery>(query);
return results.Total;
}
private async Task<List<string>> GetLemmasForWord(SearchedWord word)
{
if (!string.IsNullOrEmpty(word.Form))
{
return await lemmatizationService.GetLemmas(word.Form);
}
return new List<string>();
}
}
@@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.Concordances;
public abstract class BaseConcordancesQueryFactory
{
private readonly ILemmatizationService lemmatizationService;
protected BaseConcordancesQueryFactory(ILemmatizationService lemmatizationService)
{
this.lemmatizationService = lemmatizationService;
}
protected async Task<ConcordancesQuery> GetQuery<TRequest, TResponse>(TRequest request)
where TRequest : BaseSearchConcordances<TResponse>
{
// Return query
return new ConcordancesQuery
{
MainWord = await GetMainWordQuery(request.MainWord),
TextIds = request.TextIds,
WordsInContext = await GetWordInContextQueries(request.WordsInContext),
};
}
private static List<int> GetPositions(SearchedWordInContext wordInContext)
{
var positions = new List<int>();
AddPositions(wordInContext.LeftPosition, wordInContext.DistanceType, true);
AddPositions(wordInContext.RightPosition, wordInContext.DistanceType, false);
return positions;
void AddPositions(int position, DistanceType distanceType, bool negative)
{
if (position == 0)
{
return;
}
if (distanceType == DistanceType.Position)
{
positions.Add(negative ? -position : position);
}
else
{
for (var i = 1; i <= position; i++)
{
positions.Add(negative ? -i : i);
}
}
}
}
private async Task<SearchedMainWordQuery> GetMainWordQuery(SearchedWord mainWord)
{
return await GetWordQuery<SearchedMainWordQuery>(mainWord);
}
private async Task<List<SearchedWordInContextQuery>> GetWordInContextQueries(List<SearchedWordInContext> wordsInContext)
{
if (wordsInContext.IsNullOrEmpty())
{
return null;
}
var wordInContextQueries = new List<SearchedWordInContextQuery>();
foreach (var wordInContext in wordsInContext)
{
var wordInContextQuery = await GetWordInContextQuery(wordInContext);
wordInContextQueries.Add(wordInContextQuery);
}
return wordInContextQueries;
}
private async Task<SearchedWordInContextQuery> GetWordInContextQuery(SearchedWordInContext wordInContext)
{
var query = await GetWordQuery<SearchedWordInContextQuery>(wordInContext);
query.ConditionType = wordInContext.ConditionType;
query.Positions = GetPositions(wordInContext);
return query;
}
private async Task<T> GetWordQuery<T>(SearchedWord word)
where T : SearchedWordQuery, new()
{
var query = new T();
var lemmas = (List<string>)null;
if (!string.IsNullOrEmpty(word.Lemma))
{
lemmas = new List<string>
{
word.Lemma,
};
}
if (!string.IsNullOrEmpty(word.Form))
{
if (word.FormSearchType == FormSearchType.ExactForm)
{
query.Form = word.Form;
}
else
{
if (lemmas.IsNullOrEmpty())
{
lemmas = await lemmatizationService.GetLemmas(word.Form);
}
}
}
query.Lemmas = lemmas;
return query;
}
}
@@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.Concordances;
public class ConcordanceDetailsQueryFactory : BaseConcordancesQueryFactory, IQueryFactory<ConcordanceDetails, ConcordancesQuery>
{
public ConcordanceDetailsQueryFactory(ILemmatizationService lemmatizationService)
: base(lemmatizationService)
{
}
public async Task<ConcordancesQuery> GetQuery(ConcordanceDetails request)
{
var query = await GetQuery<ConcordanceDetails, ConcordanceDetailsResponse>(request);
query.From = 0;
query.Size = 0;
return query;
}
}
@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.Concordances;
public class ExportConcordancesQueryFactory : BaseConcordancesQueryFactory, IQueryFactory<ExportConcordances, ConcordancesQuery>
{
public ExportConcordancesQueryFactory(ILemmatizationService lemmatizationService)
: base(lemmatizationService)
{
}
public async Task<ConcordancesQuery> GetQuery(ExportConcordances request)
{
var query = await GetQuery<ExportConcordances, ExportConcordancesResponse>(request);
query.From = 0;
query.ReturnRandomRows = request.Type == ConcordanceExportType.RandomRows;
query.Size = request.Rows;
return query;
}
}
@@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.Concordances;
public class SearchConcordancesQueryFactory : BaseConcordancesQueryFactory, IQueryFactory<SearchConcordances, ConcordancesQuery>
{
public SearchConcordancesQueryFactory(ILemmatizationService lemmatizationService)
: base(lemmatizationService)
{
}
public async Task<ConcordancesQuery> GetQuery(SearchConcordances request)
{
var query = await GetQuery<SearchConcordances, SearchConcordancesResponse>(request);
query.WithPageInfo(request);
return query;
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries;
namespace Rsdo.Concordancer.Services.Search.QueryFactories;
public interface IQueryFactory<TRequest, TQuery>
where TQuery : Query
{
Task<TQuery> GetQuery(TRequest request);
}
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries.TermLists;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.Services.Services.InputQueryParser;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.TermLists;
public class BaseTermListQueryFactory
{
private readonly IInputQueryParser inputQueryParser;
private readonly ILemmatizationService lemmatizationService;
public BaseTermListQueryFactory(IInputQueryParser inputQueryParser, ILemmatizationService lemmatizationService)
{
this.inputQueryParser = inputQueryParser;
this.lemmatizationService = lemmatizationService;
}
protected async Task<TermListQuery> GetQuery<TRequest, TResponse>(TRequest request)
where TRequest : BaseSearchTermList<TResponse>
{
// Parse query
var words = await inputQueryParser.ParseDefault(request.Query);
var wordQueries = new List<SearchedTermQuery>();
foreach (var word in words)
{
var wordQuery = new SearchedTermQuery();
if (word.inPhrase)
{
wordQuery.Form = word.word;
}
else
{
wordQuery.Lemmas = await lemmatizationService.GetLemmas(word.word);
}
wordQueries.Add(wordQuery);
}
// Return query
return new TermListQuery
{
Words = wordQueries,
};
}
}
@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries.TermLists;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.Services.Services.InputQueryParser;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.TermLists;
public class ExportTermListQueryFactory : BaseTermListQueryFactory, IQueryFactory<ExportTermList, TermListQuery>
{
public ExportTermListQueryFactory(IInputQueryParser inputQueryParser, ILemmatizationService lemmatizationService)
: base(inputQueryParser, lemmatizationService)
{
}
public async Task<TermListQuery> GetQuery(ExportTermList request)
{
var query = await GetQuery<ExportTermList, ExportTermListResponse>(request);
query.From = 0;
query.Size = request.Rows;
return query;
}
}
@@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Search.Queries.TermLists;
using Rsdo.Concordancer.ServiceModel.Requests.TermLists;
using Rsdo.Concordancer.Services.Services.InputQueryParser;
using Rsdo.Concordancer.Services.Services.LemmatizationService;
namespace Rsdo.Concordancer.Services.Search.QueryFactories.TermLists;
public class SearchTermListQueryFactory : BaseTermListQueryFactory, IQueryFactory<SearchTermList, TermListQuery>
{
public SearchTermListQueryFactory(IInputQueryParser inputQueryParser, ILemmatizationService lemmatizationService)
: base(inputQueryParser, lemmatizationService)
{
}
public async Task<TermListQuery> GetQuery(SearchTermList request)
{
var query = await GetQuery<SearchTermList, SearchTermListResponse>(request);
query.WithPageInfo(request);
return query;
}
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
namespace Rsdo.Concordancer.Services.Services.InputQueryParser;
public interface IInputQueryParser
{
Task<(SearchedMainWord mainWord, List<SearchedWordInContext> wordsInContext)> Parse(string query);
Task<List<(string word, bool inPhrase)>> ParseDefault(string query);
}
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autofac.Features.Indexed;
using Rsdo.Concordancer.ServiceModel.Requests.Concordances;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Services.TokenizerService;
namespace Rsdo.Concordancer.Services.Services.InputQueryParser;
public class InputQueryParser : IInputQueryParser
{
private readonly IIndex<TokenizerType, ITokenizerService> tokenizers;
public InputQueryParser(IIndex<TokenizerType, ITokenizerService> tokenizers)
{
this.tokenizers = tokenizers;
}
public async Task<IEnumerable<(TokenType type, string form)>> Tokenize(string query)
{
try
{
return await tokenizers[TokenizerType.Classla].Tokenize(query);
}
catch (Exception)
{
return await tokenizers[TokenizerType.Default].Tokenize(query);
}
}
public async Task<(SearchedMainWord mainWord, List<SearchedWordInContext> wordsInContext)> Parse(string query)
{
if (string.IsNullOrEmpty(query))
{
throw new ArgumentNullException(query);
}
var parsed = await ParseDefault(query);
var mainWord = new SearchedMainWord()
{
Form = parsed[0].word,
FormSearchType = GetFormSearchType(parsed[0].inPhrase),
};
var wordsInContext = parsed.Skip(1)
.Select(
(w, i) => new SearchedWordInContext()
{
ConditionType = ConditionType.Is,
Form = w.word,
FormSearchType = GetFormSearchType(w.inPhrase),
DistanceType = DistanceType.Position,
LeftPosition = 0,
RightPosition = i + 1,
})
.ToList();
return (mainWord, wordsInContext);
}
public async Task<List<(string word, bool inPhrase)>> ParseDefault(string query)
{
if (string.IsNullOrEmpty(query))
{
throw new ArgumentNullException(query);
}
var result = new List<(string word, bool inPhrase)>();
var buffer = new StringBuilder();
var inPhrase = false;
for (int i = 0; i < query.Length; i++)
{
var c = query[i];
if (c != '"')
{
buffer.Append(c);
}
if (c == '"' || i == query.Length - 1)
{
var tokens = await Tokenize(buffer.ToString());
foreach (var token in tokens)
{
result.Add((token.form, inPhrase));
}
buffer.Clear();
inPhrase = !inPhrase;
}
}
return result;
}
private static FormSearchType GetFormSearchType(bool inPhrase)
{
return inPhrase ? FormSearchType.ExactForm : FormSearchType.AllForms;
}
}
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Services.Services.LemmatizationService;
public interface ILemmatizationService
{
Task<List<string>> GetLemmas(string form);
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Services.LemmatizationService;
public class LemmatizationService : ILemmatizationService
{
private readonly MasterDbContext dbContext;
public LemmatizationService(MasterDbContext dbContext)
{
this.dbContext = dbContext;
}
public async Task<List<string>> GetLemmas(string form)
{
var lemmas = await dbContext.LemmaFormPair.Where(f => f.Form.ToLower() == form.ToLower()).Select(f => f.Lemma).Distinct().ToListAsync();
if (lemmas.IsNullOrEmpty())
{
lemmas.Add(form);
}
return lemmas;
}
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.ServiceModel.Shared;
namespace Rsdo.Concordancer.Services.Services.ParagraphService;
public interface IParagraphService
{
Task<Token> GetToken(Guid tokenId);
Task<List<ConcordanceToken>> GetTokens(Expression<Func<Token, bool>> predicate);
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.Services.Framework.DbContext;
using Rsdo.Concordancer.Services.Services.PartOfSpeechService;
namespace Rsdo.Concordancer.Services.Services.ParagraphService;
public class ParagraphService : IParagraphService
{
private readonly CorpusDbContext dbContext;
private readonly IPartOfSpeechService partOfSpeechService;
public ParagraphService(CorpusDbContext dbContext, IPartOfSpeechService partOfSpeechService)
{
this.dbContext = dbContext;
this.partOfSpeechService = partOfSpeechService;
}
public async Task<Token> GetToken(Guid tokenId)
{
return await dbContext.Token.Include(t => t.Sentence).ThenInclude(s => s.Paragraph).SingleAsync(t => t.Id == tokenId);
}
public async Task<List<ConcordanceToken>> GetTokens(Expression<Func<Token, bool>> predicate)
{
var tokens = await dbContext.Token.Include(t => t.Sentence)
.ThenInclude(s => s.Paragraph)
.Where(predicate)
.OrderBy(t => t.Sentence.RecordOrder)
.ThenBy(t => t.RecordOrder)
.ToListAsync();
var concordanceTokens = new List<ConcordanceToken>();
foreach (var token in tokens)
{
concordanceTokens.Add(
new ConcordanceToken()
{
Form = token.Form,
Lemma = token.Lemma,
Msd = token.Msd,
MsdDescription = await partOfSpeechService.GetMsdDescriptionByCode(token.Msd),
TokenOrder = token.TokenOrder,
Type = token.Type,
});
}
return concordanceTokens;
}
}
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Services.Services.PartOfSpeechService;
public interface IPartOfSpeechService
{
Task<string> GetMsdDescriptionByCode(string code);
}
@@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Rsdo.Concordancer.Core.Constants;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Exceptions;
using Rsdo.Concordancer.ServiceModel.Types;
using Rsdo.Concordancer.Services.Framework.DbContext;
namespace Rsdo.Concordancer.Services.Services.PartOfSpeechService;
public class PartOfSpeechService : IPartOfSpeechService
{
private readonly MasterDbContext dbContext;
private readonly IMemoryCache memoryCache;
public PartOfSpeechService(MasterDbContext dbContext, IMemoryCache memoryCache)
{
this.dbContext = dbContext;
this.memoryCache = memoryCache;
}
public async Task<string> GetMsdDescriptionByCode(string code)
{
if (string.IsNullOrEmpty(code))
{
return null;
}
var cacheKey = Cache.CacheKeys.Msd.ByCode(code);
var msd = memoryCache.Get<Msd>(cacheKey);
if (msd == null)
{
msd = await dbContext.Msd.SingleOrDefaultAsync(m => m.Code == code);
if (msd == null)
{
throw new XNotFoundException(Errors.NotFound.EntityNotFound(EntityType.Msd, nameof(Msd.Code), code));
}
memoryCache.Set(cacheKey, msd, Cache.Duration.Long);
}
return msd.Description;
}
}
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Xml;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Services.TokenizerService;
public abstract class BaseTokenizerService : ITokenizerService
{
public async Task<IEnumerable<(TokenType type, string form)>> Tokenize(string query)
{
var xml = await GetXml(query);
return ReadTokens(xml);
}
protected abstract Task<string> GetXml(string query);
private static IEnumerable<(TokenType type, string form)> ReadTokens(string xml)
{
var tokens = new List<(TokenType type, string form)>();
using TextReader textReader = new StringReader(xml);
var readerSettings = new XmlReaderSettings
{
ConformanceLevel = ConformanceLevel.Fragment,
};
using var xmlReader = XmlReader.Create(textReader, readerSettings);
while (xmlReader.Read())
{
if (xmlReader.NodeType == XmlNodeType.Element)
{
switch (xmlReader.LocalName)
{
case "c":
xmlReader.Read(); // To move to text node
tokens.Add((TokenType.Character, xmlReader.Value));
break;
case "w":
xmlReader.Read(); // To move to text node
tokens.Add((TokenType.Word, xmlReader.Value));
break;
}
}
}
return tokens;
}
}
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Rsdo.Concordancer.Core.Constants;
namespace Rsdo.Concordancer.Services.Services.TokenizerService;
public class ClasslaTokenizerService : BaseTokenizerService
{
private readonly IConfiguration configuration;
private readonly IHttpClientFactory httpClientFactory;
private readonly ILogger<ClasslaTokenizerService> logger;
private readonly IMemoryCache memoryCache;
public ClasslaTokenizerService(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<ClasslaTokenizerService> logger,
IMemoryCache memoryCache)
{
this.configuration = configuration;
this.httpClientFactory = httpClientFactory;
this.logger = logger;
this.memoryCache = memoryCache;
}
protected override async Task<string> GetXml(string query)
{
var cacheKey = Cache.CacheKeys.Tokenizer.ByQuery(query);
var tokenized = memoryCache.Get<string>(cacheKey);
if (tokenized == null)
{
tokenized = await GetXmlFromWebService(query);
memoryCache.Set(cacheKey, tokenized, Cache.Duration.Short);
}
return tokenized;
}
private async Task<string> GetXmlFromWebService(string query)
{
var data = new[]
{
new KeyValuePair<string, string>("text", query),
new KeyValuePair<string, string>("tags", string.Empty),
new KeyValuePair<string, string>("model", "standard"),
new KeyValuePair<string, string>("text-type", "raw"),
new KeyValuePair<string, string>("tag-language", "slo"),
new KeyValuePair<string, string>("synt-dep", "ud+jos"),
new KeyValuePair<string, string>("morph-scheme", "ud+jos"),
};
var url = configuration[ConfigurationKey.Tokenizer.ClasslaTokenizerUrl];
var client = httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromMilliseconds(2000);
try
{
var response = await client.PostAsync(url, new FormUrlEncodedContent(data));
if (response.IsSuccessStatusCode)
{
using (var streamReader = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
using (var jsonReader = new JsonTextReader(streamReader))
{
var json = await JObject.LoadAsync(jsonReader);
return json["tei"].Value<string>();
}
}
}
throw new Exception($"Invalid status code: {(int)response.StatusCode}.");
}
catch (Exception e)
{
logger.LogError(e, "Error when tokenizing query '{query}'.", query);
throw;
}
}
}
@@ -0,0 +1,14 @@
using System.Linq;
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Services.Services.TokenizerService;
public class DefaultTokenizerService : BaseTokenizerService
{
protected override Task<string> GetXml(string query)
{
var words = query.Split(' ').Where(s => !string.IsNullOrEmpty(s)).ToList();
var xml = "<p>" + string.Join(string.Empty, words.Select(w => $"<w>{w}</w>").ToArray()) + "</p>";
return Task.FromResult(xml);
}
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Services.Services.TokenizerService;
public interface ITokenizerService
{
Task<IEnumerable<(TokenType type, string form)>> Tokenize(string query);
}
@@ -0,0 +1,7 @@
namespace Rsdo.Concordancer.Services.Services.TokenizerService;
public enum TokenizerType
{
Classla,
Default,
}