Initial code commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
+124
@@ -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);
|
||||
}
|
||||
}
|
||||
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
+56
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+106
@@ -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();
|
||||
}
|
||||
}
|
||||
+72
@@ -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);
|
||||
}
|
||||
}
|
||||
+57
@@ -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();
|
||||
}
|
||||
}
|
||||
+48
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
}
|
||||
+47
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -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();
|
||||
}
|
||||
}
|
||||
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
+39
@@ -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();
|
||||
}
|
||||
}
|
||||
+47
@@ -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();
|
||||
}
|
||||
}
|
||||
+81
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+139
@@ -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);
|
||||
}
|
||||
}
|
||||
+40
@@ -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();
|
||||
}
|
||||
}
|
||||
+69
@@ -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();
|
||||
}
|
||||
}
|
||||
+39
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
@@ -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);
|
||||
}
|
||||
+154
@@ -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>();
|
||||
}
|
||||
}
|
||||
+122
@@ -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;
|
||||
}
|
||||
}
|
||||
+22
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
}
|
||||
+22
@@ -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);
|
||||
}
|
||||
+50
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
+9
@@ -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);
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Rsdo.Concordancer.Services.Services.PartOfSpeechService;
|
||||
|
||||
public interface IPartOfSpeechService
|
||||
{
|
||||
Task<string> GetMsdDescriptionByCode(string code);
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+86
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user