Initial code commit

This commit is contained in:
2022-12-06 09:03:41 +01:00
parent d106d21e7d
commit 49d6401bfa
374 changed files with 48980 additions and 0 deletions
@@ -0,0 +1,36 @@
using System;
namespace Rsdo.Concordancer.Core.Constants;
public static class Cache
{
public static class Duration
{
public static TimeSpan Short => TimeSpan.FromHours(1);
public static TimeSpan Long => TimeSpan.FromDays(7);
}
public static class CacheKeys
{
public static class Msd
{
private const string Prefix = nameof(Msd);
public static string ByCode(string code)
{
return $"{Prefix}_{nameof(ByCode)}_{code}";
}
}
public static class Tokenizer
{
private const string Prefix = nameof(Tokenizer);
public static string ByQuery(string query)
{
return $"{Prefix}_{nameof(ByQuery)}_{query}";
}
}
}
}
@@ -0,0 +1,24 @@
namespace Rsdo.Concordancer.Core.Constants;
public static class ConfigurationKey
{
public static class Database
{
public const string MasterConnectionString = "RSDO:Database:MasterConnectionString";
}
public static class Search
{
public const string ElasticConnectionString = "RSDO:Elastic:ConnectionString";
}
public static class Tokenizer
{
public const string ClasslaTokenizerUrl = "RSDO:Tokenizer:ClasslaUrl";
}
public static class Web
{
public const string BaseAppPath = "RSDO:Web:BaseAppPath";
}
}
@@ -0,0 +1,9 @@
namespace Rsdo.Concordancer.Core.Constants;
public static class Constants
{
public static class Export
{
public const string DefaultContentType = "text/plain";
}
}
@@ -0,0 +1,12 @@
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Entities;
public class Corpus : Entity
{
public string Description { get; set; }
public CorpusStatus Status { get; set; }
public string Title { get; set; }
}
@@ -0,0 +1,14 @@
using System;
namespace Rsdo.Concordancer.Core.Entities;
public abstract class Entity
{
public long AutoId { get; set; }
public DateTime CreatedDate { get; set; }
public Guid Id { get; set; }
public DateTime ModifiedDate { get; set; }
}
@@ -0,0 +1,8 @@
namespace Rsdo.Concordancer.Core.Entities;
public class LemmaFormPair : Entity
{
public string Form { get; set; }
public string Lemma { get; set; }
}
@@ -0,0 +1,10 @@
namespace Rsdo.Concordancer.Core.Entities;
public class Msd : Entity
{
public string Code { get; set; }
public string Description { get; set; }
public string EnglishDescription { get; set; }
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Entities;
public class Paragraph : Entity
{
public int RecordOrder { get; set; }
public List<Sentence> Sentences { get; set; }
public Text Text { get; set; }
public long TextAutoId { get; set; }
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Entities;
public class Sentence : Entity
{
public Paragraph Paragraph { get; set; }
public long ParagraphAutoId { get; set; }
public int RecordOrder { get; set; }
public List<Token> Tokens { get; set; }
}
@@ -0,0 +1,18 @@
namespace Rsdo.Concordancer.Core.Entities;
public class Term : Entity
{
public string Form { get; set; }
public int Frequency { get; set; }
public string Lemma { get; set; }
public string Msd { get; set; }
public TermList TermList { get; set; }
public long TermListAutoId { get; set; }
public decimal Weight { get; set; }
}
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Entities;
public class TermList : Entity
{
public string SourceFile { get; set; }
public ImportStatus Status { get; set; }
public List<Term> Terms { get; set; }
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.IO;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Entities;
public class Text : Entity
{
public string Author { get; set; }
public List<Paragraph> Paragraphs { get; set; }
public string SourceFile { get; set; }
public ImportStatus Status { get; set; }
public string Title { get; set; }
public short? Year { get; set; }
public string DisplayFileName => Path.GetFileNameWithoutExtension(SourceFile);
}
@@ -0,0 +1,22 @@
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Entities;
public class Token : Entity
{
public string Form { get; set; }
public string Lemma { get; set; }
public string Msd { get; set; }
public int RecordOrder { get; set; }
public Sentence Sentence { get; set; }
public long SentenceAutoId { get; set; }
public int TokenOrder { get; set; }
public TokenType Type { get; set; }
}
@@ -0,0 +1,34 @@
using System;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Exceptions;
public static class Errors
{
public static class Forbidden
{
public static string CorpusStatusIsNotValid(CorpusStatus currentStatus, CorpusStatus requiredStatus)
{
return $"To perform selected action, corpus status must be {requiredStatus.ToString()}. Current status is {currentStatus.ToString()}.";
}
}
public static class NotFound
{
public static string EntityNotFound(EntityType entityType, Guid id)
{
return EntityNotFound(entityType, nameof(Entity.Id), id.ToString());
}
public static string EntityNotFound(EntityType entityType, string columnName, string value)
{
return $"Entity {entityType.ToString()} with {columnName}={value} not found.";
}
public static string FileNotFound(string path)
{
return $"$File {path} not found.";
}
}
}
@@ -0,0 +1,20 @@
using System;
namespace Rsdo.Concordancer.Core.Exceptions;
public class XBadRequestException : Exception
{
public XBadRequestException()
{
}
public XBadRequestException(string message)
: base(message)
{
}
public XBadRequestException(string message, Exception inner)
: base(message, inner)
{
}
}
@@ -0,0 +1,20 @@
using System;
namespace Rsdo.Concordancer.Core.Exceptions;
public class XForbiddenException : Exception
{
public XForbiddenException()
{
}
public XForbiddenException(string message)
: base(message)
{
}
public XForbiddenException(string message, Exception inner)
: base(message, inner)
{
}
}
@@ -0,0 +1,20 @@
using System;
namespace Rsdo.Concordancer.Core.Exceptions;
public class XInternalErrorException : Exception
{
public XInternalErrorException()
{
}
public XInternalErrorException(string message)
: base(message)
{
}
public XInternalErrorException(string message, Exception inner)
: base(message, inner)
{
}
}
@@ -0,0 +1,20 @@
using System;
namespace Rsdo.Concordancer.Core.Exceptions;
public class XNotFoundException : Exception
{
public XNotFoundException()
{
}
public XNotFoundException(string message)
: base(message)
{
}
public XNotFoundException(string message, Exception inner)
: base(message, inner)
{
}
}
@@ -0,0 +1,57 @@
using System;
namespace Rsdo.Concordancer.Core.Extensions;
public static class ArrayExtensions
{
public static void ForEach(this Array array, Action<Array, int[]> action)
{
if (array.LongLength == 0)
{
return;
}
var walker = new ArrayTraverse(array);
do
{
action(array, walker.Position);
}
while (walker.Step());
}
}
internal class ArrayTraverse
{
public int[] Position;
private readonly int[] maxLengths;
public ArrayTraverse(Array array)
{
maxLengths = new int[array.Rank];
for (var i = 0; i < array.Rank; ++i)
{
maxLengths[i] = array.GetLength(i) - 1;
}
Position = new int[array.Rank];
}
public bool Step()
{
for (var i = 0; i < Position.Length; ++i)
{
if (Position[i] < maxLengths[i])
{
Position[i]++;
for (var j = 0; j < i; j++)
{
Position[j] = 0;
}
return true;
}
}
return false;
}
}
@@ -0,0 +1,80 @@
using System;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Core.Model;
namespace Rsdo.Concordancer.Core.Extensions;
public static class ConcordanceExtensions
{
public static void SetToken(this Concordance concordance, Token token, int position)
{
switch (position)
{
case -10:
concordance.TokenLeft10 = token;
break;
case -9:
concordance.TokenLeft9 = token;
break;
case -8:
concordance.TokenLeft8 = token;
break;
case -7:
concordance.TokenLeft7 = token;
break;
case -6:
concordance.TokenLeft6 = token;
break;
case -5:
concordance.TokenLeft5 = token;
break;
case -4:
concordance.TokenLeft4 = token;
break;
case -3:
concordance.TokenLeft3 = token;
break;
case -2:
concordance.TokenLeft2 = token;
break;
case -1:
concordance.TokenLeft1 = token;
break;
case 0:
concordance.Token = token;
break;
case 1:
concordance.TokenRight1 = token;
break;
case 2:
concordance.TokenRight2 = token;
break;
case 3:
concordance.TokenRight3 = token;
break;
case 4:
concordance.TokenRight4 = token;
break;
case 5:
concordance.TokenRight5 = token;
break;
case 6:
concordance.TokenRight6 = token;
break;
case 7:
concordance.TokenRight7 = token;
break;
case 8:
concordance.TokenRight8 = token;
break;
case 9:
concordance.TokenRight9 = token;
break;
case 10:
concordance.TokenRight10 = token;
break;
default:
throw new ArgumentException($"Invalid position {position}. Position should be between -10 and 10.", nameof(position));
}
}
}
@@ -0,0 +1,23 @@
using System;
using Rsdo.Concordancer.Core.Entities;
namespace Rsdo.Concordancer.Core.Extensions;
public static class EntityExtensions
{
public static TEntity ApplyCreateValues<TEntity>(this TEntity entity)
where TEntity : Entity
{
entity.Id = Guid.NewGuid();
entity.CreatedDate = DateTime.Now;
entity.ModifiedDate = entity.CreatedDate;
return entity;
}
public static TEntity ApplyUpdateValues<TEntity>(this TEntity entity)
where TEntity : Entity
{
entity.ModifiedDate = DateTime.Now;
return entity;
}
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
namespace Rsdo.Concordancer.Core.Extensions;
public static class EnumerableExtensions
{
public static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences)
{
IEnumerable<IEnumerable<T>> emptyProduct = new[]
{
Enumerable.Empty<T>(),
};
return sequences.Aggregate(
emptyProduct,
(accumulator, sequence) => from accseq in accumulator
from item in sequence
select accseq.Concat(
new[]
{
item,
}));
}
public static bool IsNullOrEmpty<TSource>(this IEnumerable<TSource> collection)
{
return collection == null || !collection.Any();
}
}
@@ -0,0 +1,18 @@
using System;
using Rsdo.Concordancer.ServiceModel.Shared;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Extensions;
public static class ExecutionResultExtensions
{
public static ExecutionResult WithEntityInfo(this ExecutionResult instance, EntityType entityType, Guid entityId)
{
instance.EntityInfo = new EntityInfo()
{
EntityType = entityType,
Id = entityId,
};
return instance;
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Rsdo.Concordancer.Core.Extensions;
public static class ObjectExtensions
{
private static readonly MethodInfo CloneMethod = typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance);
public static object Copy(this object originalObject)
{
return InternalCopy(originalObject, new Dictionary<object, object>(new ReferenceEqualityComparer()));
}
public static T Copy<T>(this T original)
{
return (T)Copy((object)original);
}
public static bool IsPrimitive(this Type type)
{
if (type == typeof(string))
{
return true;
}
return type.IsValueType & type.IsPrimitive;
}
private static void CopyFields(
object originalObject,
IDictionary<object, object> visited,
object cloneObject,
Type typeToReflect,
BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy,
Func<FieldInfo, bool> filter = null)
{
foreach (var fieldInfo in typeToReflect.GetFields(bindingFlags))
{
if (filter != null && filter(fieldInfo) == false)
{
continue;
}
if (IsPrimitive(fieldInfo.FieldType))
{
continue;
}
var originalFieldValue = fieldInfo.GetValue(originalObject);
var clonedFieldValue = InternalCopy(originalFieldValue, visited);
fieldInfo.SetValue(cloneObject, clonedFieldValue);
}
}
private static object InternalCopy(object originalObject, IDictionary<object, object> visited)
{
if (originalObject == null)
{
return null;
}
var typeToReflect = originalObject.GetType();
if (IsPrimitive(typeToReflect))
{
return originalObject;
}
if (visited.ContainsKey(originalObject))
{
return visited[originalObject];
}
if (typeof(Delegate).IsAssignableFrom(typeToReflect))
{
return null;
}
var cloneObject = CloneMethod.Invoke(originalObject, null);
if (typeToReflect.IsArray)
{
var arrayType = typeToReflect.GetElementType();
if (IsPrimitive(arrayType) == false)
{
var clonedArray = (Array)cloneObject;
clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), visited), indices));
}
}
visited.Add(originalObject, cloneObject);
CopyFields(originalObject, visited, cloneObject, typeToReflect);
RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect);
return cloneObject;
}
private static void RecursiveCopyBaseTypePrivateFields(object originalObject, IDictionary<object, object> visited, object cloneObject, Type typeToReflect)
{
if (typeToReflect.BaseType != null)
{
RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect.BaseType);
CopyFields(originalObject, visited, cloneObject, typeToReflect.BaseType, BindingFlags.Instance | BindingFlags.NonPublic, info => info.IsPrivate);
}
}
}
public class ReferenceEqualityComparer : EqualityComparer<object>
{
public override bool Equals(object x, object y)
{
return ReferenceEquals(x, y);
}
public override int GetHashCode(object obj)
{
if (obj == null)
{
return 0;
}
return obj.GetHashCode();
}
}
@@ -0,0 +1,14 @@
using Rsdo.Concordancer.Core.Search.Queries;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Core.Extensions;
public static class QueryExtensions
{
public static Query WithPageInfo(this Query query, IPagedSearch request)
{
query.From = request.From;
query.Size = request.Size;
return query;
}
}
@@ -0,0 +1,15 @@
using System.Threading;
using Rsdo.Concordancer.Core.Interfaces;
namespace Rsdo.Concordancer.Core.Framework;
public static class CurrentContext
{
private static readonly AsyncLocal<ICurrentContext> CurrentContextValue = new();
public static ICurrentContext Current
{
get => CurrentContextValue.Value;
set => CurrentContextValue.Value = value;
}
}
@@ -0,0 +1,9 @@
using System;
using Rsdo.Concordancer.Core.Interfaces;
namespace Rsdo.Concordancer.Core.Framework.CurrentContexts;
public abstract class CurrentContextBase : ICurrentContext
{
public Guid CorpusId { get; set; }
}
@@ -0,0 +1,5 @@
namespace Rsdo.Concordancer.Core.Framework.CurrentContexts;
public class DefaultCurrentContext : CurrentContextBase
{
}
@@ -0,0 +1,5 @@
namespace Rsdo.Concordancer.Core.Framework.CurrentContexts;
public class SystemCurrentContext : CurrentContextBase
{
}
@@ -0,0 +1,14 @@
using System;
namespace Rsdo.Concordancer.Core.Interfaces;
public interface IConnectionStringProvider
{
string GetCorpusConnectionString();
string GetCorpusConnectionString(Guid corpusId);
string GetCorpusDatabaseName();
string GetMasterConnectionString();
}
@@ -0,0 +1,8 @@
using System;
namespace Rsdo.Concordancer.Core.Interfaces;
public interface ICurrentContext
{
Guid CorpusId { get; set; }
}
@@ -0,0 +1,12 @@
namespace Rsdo.Concordancer.Core.Interfaces;
public enum MigrationTag
{
Master,
Corpus,
}
public interface IDatabaseMigrationRunner
{
void MigrateUp(string connectionString, MigrationTag migrationTag);
}
@@ -0,0 +1,14 @@
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Core.Interfaces;
public interface IMediator
{
Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);
[DisplayName("{0}")]
Task<TResponse> Send<TResponse>(string requestName, IRequest<TResponse> request, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Core.Interfaces;
public interface IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using Rsdo.Concordancer.ServiceModel.Interfaces;
namespace Rsdo.Concordancer.Core.Interfaces;
public interface IServiceBus
{
Task Send<TResponse>(IRequest<TResponse> request);
}
@@ -0,0 +1,53 @@
using System;
using Rsdo.Concordancer.Core.Entities;
namespace Rsdo.Concordancer.Core.Model;
public class Concordance
{
public Guid ParagraphId { get; set; }
public Guid TextId { get; set; }
public Token Token { get; set; }
public Token TokenLeft1 { get; set; }
public Token TokenLeft2 { get; set; }
public Token TokenLeft3 { get; set; }
public Token TokenLeft4 { get; set; }
public Token TokenLeft5 { get; set; }
public Token TokenLeft6 { get; set; }
public Token TokenLeft7 { get; set; }
public Token TokenLeft8 { get; set; }
public Token TokenLeft9 { get; set; }
public Token TokenLeft10 { get; set; }
public Token TokenRight1 { get; set; }
public Token TokenRight2 { get; set; }
public Token TokenRight3 { get; set; }
public Token TokenRight4 { get; set; }
public Token TokenRight5 { get; set; }
public Token TokenRight6 { get; set; }
public Token TokenRight7 { get; set; }
public Token TokenRight8 { get; set; }
public Token TokenRight9 { get; set; }
public Token TokenRight10 { get; set; }
}
@@ -0,0 +1,18 @@
<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="Rsdo.StyleCop" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Rsdo.Concordancer.ServiceModel\Rsdo.Concordancer.ServiceModel.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries;
namespace Rsdo.Concordancer.Core.Search.Aggregations;
public interface IAggregator
{
Task<IDictionary<string, long>> Get<TQuery>(TQuery query)
where TQuery : Query;
}
@@ -0,0 +1,8 @@
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Search.Aggregations;
public interface IAggregatorFactory
{
IAggregator GetAggregator(AggregationType aggregationType);
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Rsdo.Concordancer.Core.Search.Queries;
namespace Rsdo.Concordancer.Core.Search;
public interface ISearchEngine
{
Task Add<TEntity>(IEnumerable<TEntity> entities);
Task Commit();
Task CreateSchema();
Task Delete<TEntity>(IEnumerable<Guid> entityIds);
Task DeleteSchema();
Task<QueryResult> Search<TEntity, TQuery>(TQuery query)
where TEntity : class
where TQuery : Query;
}
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Search.Queries.Concordances;
public class ConcordancesQuery : Query
{
public SearchedMainWordQuery MainWord { get; set; }
public bool ReturnRandomRows { get; set; }
public List<Guid> TextIds { get; set; }
public List<SearchedWordInContextQuery> WordsInContext { get; set; }
}
@@ -0,0 +1,5 @@
namespace Rsdo.Concordancer.Core.Search.Queries.Concordances;
public class SearchedMainWordQuery : SearchedWordQuery
{
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Core.Search.Queries.Concordances;
public class SearchedWordInContextQuery : SearchedWordQuery
{
public ConditionType ConditionType { get; set; }
public List<int> Positions { get; set; }
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Search.Queries.Concordances;
public abstract class SearchedWordQuery
{
public string Form { get; set; }
public List<string> Lemmas { get; set; }
public List<string> Msds { get; set; }
}
@@ -0,0 +1,10 @@
using System;
namespace Rsdo.Concordancer.Core.Search.Queries;
public abstract class Query
{
public int From { get; set; } = 0;
public int Size { get; set; } = 20;
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Search.Queries;
public class QueryResult
{
public List<string> EntityIds { get; set; }
public long Total { get; set; }
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Search.Queries.TermLists;
public class SearchedTermQuery
{
public string Form { get; set; }
public List<string> Lemmas { get; set; }
}
@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Core.Search.Queries.TermLists;
public class TermListQuery : Query
{
public List<SearchedTermQuery> Words { get; set; }
}
@@ -0,0 +1,17 @@
using Autofac;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Data.Framework;
using Rsdo.Concordancer.Data.Services;
namespace Rsdo.Concordancer.Data.CompositionRoot;
public class DataModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<ConnectionStringProvider>().As<IConnectionStringProvider>().SingleInstance();
builder.RegisterType<DatabaseMigrationRunner>().As<IDatabaseMigrationRunner>().SingleInstance();
}
}
@@ -0,0 +1,28 @@
using FluentMigrator;
using FluentMigrator.Builders.Create.Table;
using Rsdo.Concordancer.Core.Entities;
namespace Rsdo.Concordancer.Data.Extensions;
public static class FluentMigrationExtensions
{
public static ICreateTableColumnOptionOrWithColumnSyntax WithEntity(this ICreateTableWithColumnSyntax tableWithColumnSyntax)
{
return tableWithColumnSyntax.WithColumn(nameof(Entity.AutoId))
.AsInt64()
.PrimaryKey()
.Identity()
.NotNullable()
.WithColumn(nameof(Entity.CreatedDate))
.AsDateTime2()
.NotNullable()
.WithDefault(SystemMethods.CurrentDateTime)
.WithColumn(nameof(Entity.Id))
.AsGuid()
.NotNullable()
.WithColumn(nameof(Entity.ModifiedDate))
.AsDateTime2()
.NotNullable()
.WithDefault(SystemMethods.CurrentDateTime);
}
}
@@ -0,0 +1,55 @@
using System;
using Microsoft.Extensions.Configuration;
using Npgsql;
using Rsdo.Concordancer.Core.Constants;
using Rsdo.Concordancer.Core.Framework;
using Rsdo.Concordancer.Core.Interfaces;
namespace Rsdo.Concordancer.Data.Framework;
public class ConnectionStringProvider : IConnectionStringProvider
{
private readonly IConfiguration configuration;
public ConnectionStringProvider(IConfiguration configuration)
{
this.configuration = configuration;
}
public string GetCorpusConnectionString()
{
return GetCorpusConnectionString(CurrentContext.Current.CorpusId);
}
public string GetCorpusConnectionString(Guid corpusId)
{
var masterConnectionString = GetMasterConnectionString();
var masterBuilder = new NpgsqlConnectionStringBuilder(masterConnectionString)
{
Database = GetCorpusDatabaseName(corpusId),
};
return masterBuilder.ConnectionString;
}
public string GetCorpusDatabaseName()
{
return GetCorpusDatabaseName(CurrentContext.Current.CorpusId);
}
public string GetMasterConnectionString()
{
return configuration[ConfigurationKey.Database.MasterConnectionString];
}
private string GetMasterDatabaseName()
{
var connectionString = GetMasterConnectionString();
return new NpgsqlConnectionStringBuilder(connectionString).Database;
}
private string GetCorpusDatabaseName(Guid corpusId)
{
var masterDatabaseName = GetMasterDatabaseName();
return $"{masterDatabaseName}_{corpusId:N}";
}
}
@@ -0,0 +1,28 @@
using FluentMigrator;
using Rsdo.Concordancer.Core.Interfaces;
namespace Rsdo.Concordancer.Data.Framework;
public abstract class DbAttribute : TagsAttribute
{
protected DbAttribute(MigrationTag migrationTag)
: base(migrationTag.ToString())
{
}
}
public class CorpusDbAttribute : DbAttribute
{
public CorpusDbAttribute()
: base(MigrationTag.Corpus)
{
}
}
public class MasterDbAttribute : DbAttribute
{
public MasterDbAttribute()
: base(MigrationTag.Master)
{
}
}
@@ -0,0 +1,17 @@
using FluentMigrator.Runner.Generators.Postgres;
using FluentMigrator.Runner.Processors.Postgres;
namespace Rsdo.Concordancer.Data.Framework;
public class NoQuoteQuoter : PostgresQuoter
{
public NoQuoteQuoter(PostgresOptions options)
: base(options)
{
}
protected override bool ShouldQuote(string name)
{
return false;
}
}
@@ -0,0 +1,22 @@
using FluentMigrator.Runner.VersionTableInfo;
namespace Rsdo.Concordancer.Data.Framework;
public class VersionInfoTableMetadata : IVersionTableMetaData
{
public string ColumnName => "version";
public string TableName => "versioninfo";
public string DescriptionColumnName => "description";
public string AppliedOnColumnName => "appliedon";
public string UniqueIndexName => "uc_version";
public object ApplicationContext { get; set; }
public bool OwnsSchema => true;
public string SchemaName => "public";
}
@@ -0,0 +1,123 @@
using System.Data;
using FluentMigrator;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Data.Extensions;
using Rsdo.Concordancer.Data.Framework;
namespace Rsdo.Concordancer.Data.Migrations.Initial.Corpus;
[CorpusDb]
[Migration(20211103214800)]
public class Mig20211103214800_Initial : ForwardOnlyMigration
{
public override void Up()
{
// Text
Create.Table(nameof(Text))
.WithEntity()
.WithColumn(nameof(Text.Author))
.AsString(255)
.Nullable()
.WithColumn(nameof(Text.SourceFile))
.AsString(255)
.NotNullable()
.WithColumn(nameof(Text.Title))
.AsString(255)
.Nullable()
.WithColumn(nameof(Text.Year))
.AsInt16()
.Nullable();
Create.Index().OnTable(nameof(Text)).OnColumn(nameof(Text.Id)).Ascending().WithOptions().Unique();
// Paragraph
Create.Table(nameof(Paragraph))
.WithEntity()
.WithColumn(nameof(Paragraph.RecordOrder))
.AsInt32()
.NotNullable()
.WithColumn(nameof(Paragraph.TextAutoId))
.AsInt64()
.NotNullable()
.ForeignKey(nameof(Text), nameof(Text.AutoId))
.OnDelete(Rule.Cascade);
Create.Index().OnTable(nameof(Paragraph)).OnColumn(nameof(Paragraph.Id)).Ascending().WithOptions().Unique();
Create.Index().OnTable(nameof(Paragraph)).OnColumn(nameof(Paragraph.TextAutoId));
// Sentence
Create.Table(nameof(Sentence))
.WithEntity()
.WithColumn(nameof(Sentence.ParagraphAutoId))
.AsInt64()
.NotNullable()
.ForeignKey(nameof(Paragraph), nameof(Paragraph.AutoId))
.OnDelete(Rule.Cascade)
.WithColumn(nameof(Sentence.RecordOrder))
.AsInt32()
.NotNullable();
Create.Index().OnTable(nameof(Sentence)).OnColumn(nameof(Sentence.Id)).Ascending().WithOptions().Unique();
Create.Index().OnTable(nameof(Sentence)).OnColumn(nameof(Sentence.ParagraphAutoId));
// Token
Create.Table(nameof(Token))
.WithEntity()
.WithColumn(nameof(Token.Form))
.AsString(400)
.Nullable()
.WithColumn(nameof(Token.Lemma))
.AsString(400)
.Nullable()
.WithColumn(nameof(Token.Msd))
.AsString(20)
.Nullable()
.WithColumn(nameof(Token.RecordOrder))
.AsInt32()
.NotNullable()
.WithColumn(nameof(Token.SentenceAutoId))
.AsInt64()
.NotNullable()
.ForeignKey(nameof(Sentence), nameof(Sentence.AutoId))
.OnDelete(Rule.Cascade)
.WithColumn(nameof(Token.TokenOrder))
.AsInt32()
.NotNullable()
.WithColumn(nameof(Token.Type))
.AsByte()
.NotNullable();
Create.Index().OnTable(nameof(Token)).OnColumn(nameof(Token.Id)).Ascending().WithOptions().Unique();
Create.Index().OnTable(nameof(Token)).OnColumn(nameof(Token.SentenceAutoId)).Ascending().OnColumn(nameof(Token.TokenOrder));
// TermList
Create.Table(nameof(TermList)).WithEntity().WithColumn(nameof(Text.SourceFile)).AsString(255).NotNullable();
// Term
Create.Table(nameof(Term))
.WithEntity()
.WithColumn(nameof(Term.Form))
.AsString(400)
.NotNullable()
.WithColumn(nameof(Term.Frequency))
.AsInt32()
.NotNullable()
.WithColumn(nameof(Term.Lemma))
.AsString(400)
.NotNullable()
.WithColumn(nameof(Term.Msd))
.AsString(100)
.NotNullable()
.WithColumn(nameof(Term.TermListAutoId))
.AsInt64()
.NotNullable()
.ForeignKey(nameof(TermList), nameof(TermList.AutoId))
.OnDelete(Rule.Cascade)
.WithColumn(nameof(Term.Weight))
.AsDecimal(16, 15)
.NotNullable();
Create.Index().OnTable(nameof(Term)).OnColumn(nameof(Term.Id)).Ascending().WithOptions().Unique();
Create.Index().OnTable(nameof(Term)).OnColumn(nameof(Term.TermListAutoId)).Ascending();
}
}
@@ -0,0 +1,38 @@
using FluentMigrator;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Data.Extensions;
using Rsdo.Concordancer.Data.Framework;
namespace Rsdo.Concordancer.Data.Migrations.Initial.Master;
[MasterDb]
[Migration(20211016212200)]
public class Mig20211016212200_Initial : ForwardOnlyMigration
{
public override void Up()
{
// Corpus
Create.Table(nameof(Core.Entities.Corpus))
.WithEntity()
.WithColumn(nameof(Core.Entities.Corpus.Description))
.AsString(255)
.Nullable()
.WithColumn(nameof(Core.Entities.Corpus.Status))
.AsByte()
.NotNullable()
.WithColumn(nameof(Core.Entities.Corpus.Title))
.AsString(100)
.NotNullable();
// LemmaFormPair
Create.Table(nameof(LemmaFormPair))
.WithEntity()
.WithColumn(nameof(LemmaFormPair.Lemma))
.AsString(255)
.NotNullable()
.WithColumn(nameof(LemmaFormPair.Form))
.AsString(255)
.NotNullable();
Execute.Sql($"CREATE INDEX ix_lemmaformpair_form_lower ON lemmaformpair (lower(form))");
}
}
@@ -0,0 +1,20 @@
using FluentMigrator;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Data.Framework;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Data.Migrations.v1_0.Corpus;
[CorpusDb]
[Migration(20221015220400)]
public class Mig20221015220400_CreateTokenTypeIndex : ForwardOnlyMigration
{
public override void Up()
{
var token = nameof(Token).ToLower();
var type = nameof(Token.Type).ToLower();
var word = (int)TokenType.Word;
Execute.Sql($"CREATE INDEX ix_{token}_{type}_partial ON {token}({type}) WHERE {type} = {word}");
}
}
@@ -0,0 +1,24 @@
using FluentMigrator;
using Rsdo.Concordancer.Core.Entities;
using Rsdo.Concordancer.Data.Framework;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Data.Migrations.v1_0.Corpus;
[CorpusDb]
[Migration(20221021080000)]
public class Mig20221021080000_AddTextAndTermListStatus : ForwardOnlyMigration
{
public override void Up()
{
// Text
Alter.Table(nameof(Text)).AddColumn(nameof(Text.Status)).AsByte().Nullable();
Execute.Sql($"UPDATE {nameof(Text)} SET {nameof(Text.Status)} = {(int)ImportStatus.Active};");
Alter.Table(nameof(Text)).AlterColumn(nameof(Text.Status)).AsByte().NotNullable();
// TermList
Alter.Table(nameof(TermList)).AddColumn(nameof(TermList.Status)).AsByte().Nullable();
Execute.Sql($"UPDATE {nameof(TermList)} SET {nameof(TermList.Status)} = {(int)ImportStatus.Active};");
Alter.Table(nameof(TermList)).AlterColumn(nameof(TermList.Status)).AsByte().NotNullable();
}
}
@@ -0,0 +1,23 @@
<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="FluentMigrator" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
<PackageReference Include="Npgsql" 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,43 @@
using System;
using FluentMigrator.Runner;
using FluentMigrator.Runner.Generators.Postgres;
using FluentMigrator.Runner.Initialization;
using FluentMigrator.Runner.VersionTableInfo;
using Microsoft.Extensions.DependencyInjection;
using Rsdo.Concordancer.Core.Interfaces;
using Rsdo.Concordancer.Data.CompositionRoot;
using Rsdo.Concordancer.Data.Framework;
namespace Rsdo.Concordancer.Data.Services;
public class DatabaseMigrationRunner : IDatabaseMigrationRunner
{
public void MigrateUp(string connectionString, MigrationTag migrationTag)
{
var serviceProvider = CreateServices(connectionString, migrationTag);
using var scope = serviceProvider.CreateScope();
UpdateDatabase(scope.ServiceProvider);
}
private static IServiceProvider CreateServices(string connectionString, MigrationTag migrationTag)
{
return new ServiceCollection().AddFluentMigratorCore()
.ConfigureRunner(rb => rb.AddPostgres11_0().WithGlobalConnectionString(connectionString).ScanIn(typeof(DataModule).Assembly).For.Migrations())
.AddLogging(lb => lb.AddFluentMigratorConsole())
.Configure<RunnerOptions>(
opt => opt.Tags = new[]
{
migrationTag.ToString(),
})
.AddScoped<PostgresQuoter, NoQuoteQuoter>()
.AddScoped<IVersionTableMetaData, VersionInfoTableMetadata>()
.BuildServiceProvider(false);
}
private static void UpdateDatabase(IServiceProvider serviceProvider)
{
var runner = serviceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp();
}
}
@@ -0,0 +1,69 @@
using System.Reflection;
using Autofac;
using Microsoft.Extensions.Configuration;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Aggregations;
using Rsdo.Concordancer.Infrastructure.Search;
using Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
using Rsdo.Concordancer.Infrastructure.Search.Aggregations;
using Rsdo.Concordancer.Infrastructure.Search.Client;
using Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
using Rsdo.Concordancer.ServiceModel.Types;
using Module = Autofac.Module;
namespace Rsdo.Concordancer.Infrastructure.CompositionRoot;
public class InfrastructureModule : Module
{
private Assembly InfrastructureAssembly => GetType().Assembly;
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
RegisterConfiguration(builder);
RegisterSearch(builder);
}
private static void RegisterConfiguration(ContainerBuilder builder)
{
var configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build();
builder.RegisterInstance(configuration).As<IConfiguration>().SingleInstance();
}
private void RegisterSearch(ContainerBuilder builder)
{
// Search engine
builder.RegisterType<ElasticSearchEngine>().As<ISearchEngine>().SingleInstance();
// Elastic client
builder.RegisterType<ElasticClientFactory>().As<IElasticClientFactory>().SingleInstance();
builder.Register(
c =>
{
var factory = c.Resolve<IElasticClientFactory>();
return factory.Get();
})
.SingleInstance();
// Index providers
builder.RegisterType<IndexProviderFactory>().As<IIndexProviderFactory>().SingleInstance();
builder.RegisterAssemblyTypes(InfrastructureAssembly).AsClosedTypesOf(typeof(IIndexProvider<>)).AsImplementedInterfaces().SingleInstance();
// Add and delete records
builder.RegisterType<AddRecordsHandlerFactory>().As<IAddRecordsHandlerFactory>().SingleInstance();
builder.RegisterType<DeleteRecordsHandlerFactory>().As<IDeleteRecordsHandlerFactory>().SingleInstance();
builder.RegisterAssemblyTypes(InfrastructureAssembly).AsClosedTypesOf(typeof(IAddRecordsHandler<>)).AsImplementedInterfaces().SingleInstance();
builder.RegisterAssemblyTypes(InfrastructureAssembly).AsClosedTypesOf(typeof(IDeleteRecordsHandler<>)).AsImplementedInterfaces().SingleInstance();
// Query builders
builder.RegisterType<QueryBuilderFactory>().As<IQueryBuilderFactory>().SingleInstance();
builder.RegisterAssemblyTypes(InfrastructureAssembly).AsClosedTypesOf(typeof(IQueryBuilder<>)).AsImplementedInterfaces().SingleInstance();
// Aggregations
builder.RegisterType<AggregatorFactory>().As<IAggregatorFactory>().SingleInstance();
builder.RegisterType<TextAggregator>().Keyed<IAggregator>(AggregationType.Text).SingleInstance();
}
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using OpenSearch.Client;
namespace Rsdo.Concordancer.Infrastructure.Extensions;
public static class ElasticQueryExtensions
{
public static QueryContainer ToBooleanAndQuery(this List<QueryContainer> queries)
{
return queries.Count switch
{
0 => null,
1 => queries[0],
_ => new BoolQuery()
{
Must = queries,
},
};
}
public static QueryContainer ToBooleanOrQuery(this List<QueryContainer> queries)
{
return queries.Count switch
{
0 => null,
1 => queries[0],
_ => new BoolQuery()
{
Should = queries,
},
};
}
}
@@ -0,0 +1,22 @@
<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="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="OpenSearch.Client" Version="1.1.0" />
<PackageReference Include="Rsdo.StyleCop" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Rsdo.Concordancer.Core\Rsdo.Concordancer.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,61 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.Model;
namespace Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
public class AddConcordanceRecordsHandler : BaseAddRecordsHandler<Concordance, EsConcordance>
{
public AddConcordanceRecordsHandler(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory)
: base(client, indexProviderFactory)
{
}
protected override EsConcordance ConvertEntity(Concordance entity)
{
return new EsConcordance
{
Id = entity.Token.Id,
TextId = entity.TextId,
Token = ConvertToken(entity.Token),
TokenLeft1 = ConvertToken(entity.TokenLeft1),
TokenLeft2 = ConvertToken(entity.TokenLeft2),
TokenLeft3 = ConvertToken(entity.TokenLeft3),
TokenLeft4 = ConvertToken(entity.TokenLeft4),
TokenLeft5 = ConvertToken(entity.TokenLeft5),
TokenLeft6 = ConvertToken(entity.TokenLeft6),
TokenLeft7 = ConvertToken(entity.TokenLeft7),
TokenLeft8 = ConvertToken(entity.TokenLeft8),
TokenLeft9 = ConvertToken(entity.TokenLeft9),
TokenLeft10 = ConvertToken(entity.TokenLeft10),
TokenRight1 = ConvertToken(entity.TokenRight1),
TokenRight2 = ConvertToken(entity.TokenRight2),
TokenRight3 = ConvertToken(entity.TokenRight3),
TokenRight4 = ConvertToken(entity.TokenRight4),
TokenRight5 = ConvertToken(entity.TokenRight5),
TokenRight6 = ConvertToken(entity.TokenRight6),
TokenRight7 = ConvertToken(entity.TokenRight7),
TokenRight8 = ConvertToken(entity.TokenRight8),
TokenRight9 = ConvertToken(entity.TokenRight9),
TokenRight10 = ConvertToken(entity.TokenRight10),
};
}
private static EsToken ConvertToken(Rsdo.Concordancer.Core.Entities.Token token)
{
if (token == null)
{
return null;
}
return new EsToken
{
Form = token.Form,
FormLower = token.Form?.ToLower(),
Lemma = token.Lemma,
LemmaLower = token.Lemma?.ToLower(),
Msd = token.Msd,
};
}
}
@@ -0,0 +1,18 @@
using Autofac;
namespace Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
public class AddRecordsHandlerFactory : IAddRecordsHandlerFactory
{
private readonly ILifetimeScope lifetimeScope;
public AddRecordsHandlerFactory(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public IAddRecordsHandler<TEntity> GetHandler<TEntity>()
{
return lifetimeScope.Resolve<IAddRecordsHandler<TEntity>>();
}
}
@@ -0,0 +1,57 @@
using System;
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.Model;
using Term = Rsdo.Concordancer.Core.Entities.Term;
namespace Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
public class AddTermRecordsHandler : BaseAddRecordsHandler<Term, EsTerm>
{
public AddTermRecordsHandler(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory)
: base(client, indexProviderFactory)
{
}
protected override EsTerm ConvertEntity(Term entity)
{
// Tokenize form, lemma and msd
var forms = entity.Form.Split(' ');
var lemmas = entity.Lemma.Split(' ');
var msds = entity.Msd.Split(' ');
return new EsTerm
{
Id = entity.Id,
Frequency = entity.Frequency,
Token = GetToken(0),
TokenRight1 = GetToken(1),
TokenRight2 = GetToken(2),
TokenRight3 = GetToken(3),
TokenRight4 = GetToken(4),
Weight = entity.Weight,
};
EsToken GetToken(int index)
{
if (index > forms.Length)
{
return null;
}
return new EsToken()
{
Form = GetValue(forms, index),
Lemma = GetValue(lemmas, index),
Msd = GetValue(msds, index),
FormLower = GetValue(forms, index)?.ToLower(),
LemmaLower = GetValue(lemmas, index)?.ToLower(),
};
}
string GetValue(string[] values, int index)
{
return index < values.Length ? values[index] : null;
}
}
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Extensions;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
namespace Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
public abstract class BaseAddRecordsHandler<TEntity, TElasticEntity> : IAddRecordsHandler<TEntity>
where TElasticEntity : class
{
private readonly IOpenSearchClient client;
private readonly IIndexProviderFactory indexProviderFactory;
protected BaseAddRecordsHandler(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory)
{
this.client = client;
this.indexProviderFactory = indexProviderFactory;
}
public async Task Add(IEnumerable<TEntity> entities)
{
// Get index provider
var indexProvider = indexProviderFactory.GetProvider<TElasticEntity>();
// Get index name
var indexName = indexProvider.IndexName;
// Create request
var request = new BulkRequest(indexName)
{
Operations = new List<IBulkOperation>(),
Timeout = TimeSpan.FromMinutes(5),
};
// Convert entities to elastic entities
foreach (var entity in entities)
{
var elasticEntity = ConvertEntity(entity);
request.Operations.Add(new BulkIndexOperation<TElasticEntity>(elasticEntity));
}
// Get response
var response = await client.BulkAsync(request);
response.ThrowIfInvalid();
}
protected abstract TElasticEntity ConvertEntity(TEntity entity);
}
@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
public interface IAddRecordsHandler<TEntity>
{
Task Add(IEnumerable<TEntity> entities);
}
@@ -0,0 +1,6 @@
namespace Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
public interface IAddRecordsHandlerFactory
{
IAddRecordsHandler<TEntity> GetHandler<TEntity>();
}
@@ -0,0 +1,20 @@
using Autofac.Features.Indexed;
using Rsdo.Concordancer.Core.Search.Aggregations;
using Rsdo.Concordancer.ServiceModel.Types;
namespace Rsdo.Concordancer.Infrastructure.Search.Aggregations;
public class AggregatorFactory : IAggregatorFactory
{
private readonly IIndex<AggregationType, IAggregator> aggregators;
public AggregatorFactory(IIndex<AggregationType, IAggregator> aggregators)
{
this.aggregators = aggregators;
}
public IAggregator GetAggregator(AggregationType aggregationType)
{
return aggregators[aggregationType];
}
}
@@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Search.Aggregations;
using Rsdo.Concordancer.Core.Search.Queries;
using Rsdo.Concordancer.Infrastructure.Search.Extensions;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.Model;
using Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
namespace Rsdo.Concordancer.Infrastructure.Search.Aggregations;
public abstract class BaseAggregator : IAggregator
{
private readonly IOpenSearchClient client;
private readonly IIndexProviderFactory indexProviderFactory;
private readonly IQueryBuilderFactory queryBuilderFactory;
protected BaseAggregator(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory, IQueryBuilderFactory queryBuilderFactory)
{
this.client = client;
this.indexProviderFactory = indexProviderFactory;
this.queryBuilderFactory = queryBuilderFactory;
}
protected abstract string FieldName { get; }
public async Task<IDictionary<string, long>> Get<TQuery>(TQuery query)
where TQuery : Query
{
// Get elastic query
var queryBuilder = queryBuilderFactory.GetBuilder<TQuery>();
var elasticQuery = queryBuilder.Build(query);
// Get and execute search request
var searchRequest = GetSearchRequest(elasticQuery);
var response = await client.SearchAsync<EsConcordance>(searchRequest);
response.ThrowIfInvalid();
// Read response
return ReadAggregations(response);
}
private static IDictionary<string, long> ReadAggregations(ISearchResponse<EsConcordance> response)
{
var terms = response.Aggregations.Terms("agg");
return terms?.Buckets?.ToDictionary(x => x.Key, x => x.DocCount ?? 0);
}
private SearchRequest GetSearchRequest(QueryContainer query)
{
var indexProvider = indexProviderFactory.GetProvider<EsConcordance>();
var indexName = indexProvider.IndexName;
return new SearchRequest(indexName)
{
From = 0,
Size = 0,
Query = query,
Aggregations = new AggregationDictionary()
{
{
"agg", new TermsAggregation("terms")
{
Field = FieldName,
Size = 100,
}
},
},
};
}
}
@@ -0,0 +1,15 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
namespace Rsdo.Concordancer.Infrastructure.Search.Aggregations;
public class TextAggregator : BaseAggregator
{
public TextAggregator(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory, IQueryBuilderFactory queryBuilderFactory)
: base(client, indexProviderFactory, queryBuilderFactory)
{
}
protected override string FieldName => "textId";
}
@@ -0,0 +1,26 @@
using System;
using Microsoft.Extensions.Configuration;
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Constants;
namespace Rsdo.Concordancer.Infrastructure.Search.Client;
public class ElasticClientFactory : IElasticClientFactory
{
private readonly IConfiguration configuration;
public ElasticClientFactory(IConfiguration configuration)
{
this.configuration = configuration;
}
public IOpenSearchClient Get()
{
var connectionString = configuration[ConfigurationKey.Search.ElasticConnectionString];
var connectionSettings = new ConnectionSettings(new Uri(connectionString)).SniffOnStartup(false).RequestTimeout(TimeSpan.FromSeconds(30));
#if DEBUG
connectionSettings.EnableDebugMode().IncludeServerStackTraceOnError(false);
#endif
return new OpenSearchClient(connectionSettings);
}
}
@@ -0,0 +1,8 @@
using OpenSearch.Client;
namespace Rsdo.Concordancer.Infrastructure.Search.Client;
public interface IElasticClientFactory
{
IOpenSearchClient Get();
}
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Extensions;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
namespace Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
public abstract class BaseDeleteRecordsHandler<TEntity, TElasticEntity> : IDeleteRecordsHandler<TEntity>
where TElasticEntity : class
{
private readonly IOpenSearchClient client;
private readonly IIndexProviderFactory indexProviderFactory;
protected BaseDeleteRecordsHandler(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory)
{
this.client = client;
this.indexProviderFactory = indexProviderFactory;
}
public async Task Delete(IEnumerable<Guid> entityIds)
{
// Get index provider
var indexProvider = indexProviderFactory.GetProvider<TElasticEntity>();
// Get index name
var indexName = indexProvider.IndexName;
// Create request
var request = new BulkRequest(indexName)
{
Operations = new List<IBulkOperation>(),
Timeout = TimeSpan.FromMinutes(5),
};
// Convert entities to elastic entities
foreach (var entityId in entityIds)
{
request.Operations.Add(new BulkDeleteDescriptor<TElasticEntity>().Id(entityId));
}
// Get response
var response = await client.BulkAsync(request);
response.ThrowIfInvalid();
}
}
@@ -0,0 +1,14 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.Model;
namespace Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
public class DeleteConcordanceRecordsHandler : BaseDeleteRecordsHandler<Concordance, EsConcordance>
{
public DeleteConcordanceRecordsHandler(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory)
: base(client, indexProviderFactory)
{
}
}
@@ -0,0 +1,18 @@
using Autofac;
namespace Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
public class DeleteRecordsHandlerFactory : IDeleteRecordsHandlerFactory
{
private readonly ILifetimeScope lifetimeScope;
public DeleteRecordsHandlerFactory(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public IDeleteRecordsHandler<TEntity> GetHandler<TEntity>()
{
return lifetimeScope.Resolve<IDeleteRecordsHandler<TEntity>>();
}
}
@@ -0,0 +1,14 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.Model;
using Term = Rsdo.Concordancer.Core.Entities.Term;
namespace Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
public class DeleteTermRecordsHandler : BaseDeleteRecordsHandler<Term, EsTerm>
{
public DeleteTermRecordsHandler(IOpenSearchClient client, IIndexProviderFactory indexProviderFactory)
: base(client, indexProviderFactory)
{
}
}
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
public interface IDeleteRecordsHandler<TEntity>
{
Task Delete(IEnumerable<Guid> entityIds);
}
@@ -0,0 +1,6 @@
namespace Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
public interface IDeleteRecordsHandlerFactory
{
IDeleteRecordsHandler<TEntity> GetHandler<TEntity>();
}
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Model;
using Rsdo.Concordancer.Core.Search;
using Rsdo.Concordancer.Core.Search.Queries;
using Rsdo.Concordancer.Infrastructure.Search.AddRecordsHandlers;
using Rsdo.Concordancer.Infrastructure.Search.DeleteRecordsHandlers;
using Rsdo.Concordancer.Infrastructure.Search.Extensions;
using Rsdo.Concordancer.Infrastructure.Search.Indexes;
using Rsdo.Concordancer.Infrastructure.Search.Model;
using Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
using Term = Rsdo.Concordancer.Core.Entities.Term;
namespace Rsdo.Concordancer.Infrastructure.Search;
public class ElasticSearchEngine : ISearchEngine
{
private readonly IAddRecordsHandlerFactory addRecordsHandlerFactory;
private readonly IOpenSearchClient client;
private readonly IDeleteRecordsHandlerFactory deleteRecordsHandlerFactory;
private readonly IIndexProviderFactory indexProviderFactory;
private readonly IQueryBuilderFactory queryBuilderFactory;
public ElasticSearchEngine(
IAddRecordsHandlerFactory addRecordsHandlerFactory,
IOpenSearchClient client,
IDeleteRecordsHandlerFactory deleteRecordsHandlerFactory,
IIndexProviderFactory indexProviderFactory,
IQueryBuilderFactory queryBuilderFactory)
{
this.addRecordsHandlerFactory = addRecordsHandlerFactory;
this.client = client;
this.deleteRecordsHandlerFactory = deleteRecordsHandlerFactory;
this.indexProviderFactory = indexProviderFactory;
this.queryBuilderFactory = queryBuilderFactory;
}
public Task Add<TEntity>(IEnumerable<TEntity> entities)
{
var handler = addRecordsHandlerFactory.GetHandler<TEntity>();
return handler.Add(entities);
}
public async Task Commit()
{
foreach (var provider in indexProviderFactory.GetAllProviders())
{
await provider.Refresh();
}
}
public async Task CreateSchema()
{
foreach (var provider in indexProviderFactory.GetAllProviders())
{
await provider.Create();
}
}
public Task Delete<TEntity>(IEnumerable<Guid> entityIds)
{
var handler = deleteRecordsHandlerFactory.GetHandler<TEntity>();
return handler.Delete(entityIds);
}
public async Task DeleteSchema()
{
foreach (var provider in indexProviderFactory.GetAllProviders())
{
if (await provider.Exists())
{
await provider.Delete();
}
}
}
public async Task<QueryResult> Search<TEntity, TQuery>(TQuery query)
where TEntity : class
where TQuery : Query
{
// Get search request
var searchRequest = GetSearchRequest<TEntity, TQuery>(query);
// Execute search
var searchResponse = await client.SearchAsync<TEntity>(searchRequest);
searchResponse.ThrowIfInvalid();
// Return result
return new QueryResult()
{
EntityIds = searchResponse.Hits?.Select(h => h.Id).ToList(),
Total = searchResponse.Total,
};
}
private IIndexProvider GetIndexProvider<TEntity>()
{
// ToDo: this should be done with some kind of mapper
if (typeof(TEntity) == typeof(Concordance))
{
return indexProviderFactory.GetProvider<EsConcordance>();
}
if (typeof(TEntity) == typeof(Term))
{
return indexProviderFactory.GetProvider<EsTerm>();
}
throw new ArgumentOutOfRangeException($"Entity type {typeof(TEntity)} is not supported as a search type in Elastic!");
}
private SearchRequest GetSearchRequest<TEntity, TQuery>(TQuery query)
where TQuery : Query
{
// Get query builder
var queryBuilder = queryBuilderFactory.GetBuilder<TQuery>();
// Build elastic query
var elasticQuery = queryBuilder.Build(query);
// Get index provider
var indexProvider = GetIndexProvider<TEntity>();
// Return search request
return new SearchRequest(indexProvider.IndexName)
{
From = query.From,
Size = query.Size,
TrackTotalHits = true,
Query = elasticQuery,
};
}
}
@@ -0,0 +1,15 @@
using System;
using OpenSearch.Client;
namespace Rsdo.Concordancer.Infrastructure.Search.Extensions;
public static class ElasticResponseExtensions
{
public static void ThrowIfInvalid(this IResponse response)
{
if (!response.IsValid)
{
throw new Exception($"Invalid Elastic response: {response.DebugInformation}!");
}
}
}
@@ -0,0 +1,44 @@
using System.Threading.Tasks;
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Framework;
using Rsdo.Concordancer.Infrastructure.Search.Extensions;
namespace Rsdo.Concordancer.Infrastructure.Search.Indexes;
public abstract class BaseIndexProvider<TModel> : IIndexProvider<TModel>
where TModel : class
{
private readonly IOpenSearchClient client;
protected BaseIndexProvider(IOpenSearchClient client)
{
this.client = client;
}
public string IndexName => $"rsdo_{typeof(TModel).Name.ToLower()}_{CurrentContext.Current.CorpusId:N}";
public async Task Create()
{
var response = await client.Indices.CreateAsync(
IndexName,
c => c.Settings(s => s.NumberOfShards(3).NumberOfReplicas(0)).Map(ms => ms.AutoMap<TModel>().SourceField(sf => sf.Enabled(false))));
response.ThrowIfInvalid();
}
public async Task Delete()
{
var response = await client.Indices.DeleteAsync(IndexName);
response.ThrowIfInvalid();
}
public async Task<bool> Exists()
{
return (await client.Indices.ExistsAsync(IndexName)).Exists;
}
public async Task Refresh()
{
var response = await client.Indices.RefreshAsync(IndexName);
response.ThrowIfInvalid();
}
}
@@ -0,0 +1,12 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Model;
namespace Rsdo.Concordancer.Infrastructure.Search.Indexes;
public class ConcordanceIndexProvider : BaseIndexProvider<EsConcordance>
{
public ConcordanceIndexProvider(IOpenSearchClient client)
: base(client)
{
}
}
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
namespace Rsdo.Concordancer.Infrastructure.Search.Indexes;
public interface IIndexProvider<TModel> : IIndexProvider
{
}
public interface IIndexProvider
{
string IndexName { get; }
Task Create();
Task Delete();
Task<bool> Exists();
Task Refresh();
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Rsdo.Concordancer.Infrastructure.Search.Indexes;
public interface IIndexProviderFactory
{
IEnumerable<IIndexProvider> GetAllProviders();
IIndexProvider<TModel> GetProvider<TModel>();
}
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using Autofac;
namespace Rsdo.Concordancer.Infrastructure.Search.Indexes;
public class IndexProviderFactory : IIndexProviderFactory
{
private readonly ILifetimeScope lifetimeScope;
public IndexProviderFactory(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public IEnumerable<IIndexProvider> GetAllProviders()
{
return lifetimeScope.Resolve<IEnumerable<IIndexProvider>>();
}
public IIndexProvider<TModel> GetProvider<TModel>()
{
return lifetimeScope.Resolve<IIndexProvider<TModel>>();
}
}
@@ -0,0 +1,12 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Infrastructure.Search.Model;
namespace Rsdo.Concordancer.Infrastructure.Search.Indexes;
public class TermIndexProvider : BaseIndexProvider<EsTerm>
{
public TermIndexProvider(IOpenSearchClient client)
: base(client)
{
}
}
@@ -0,0 +1,54 @@
using System;
using OpenSearch.Client;
namespace Rsdo.Concordancer.Infrastructure.Search.Model;
public class EsConcordance
{
public Guid Id { get; set; }
[Keyword]
public Guid TextId { get; set; }
public EsToken Token { get; set; }
public EsToken TokenLeft1 { get; set; }
public EsToken TokenLeft2 { get; set; }
public EsToken TokenLeft3 { get; set; }
public EsToken TokenLeft4 { get; set; }
public EsToken TokenLeft5 { get; set; }
public EsToken TokenLeft6 { get; set; }
public EsToken TokenLeft7 { get; set; }
public EsToken TokenLeft8 { get; set; }
public EsToken TokenLeft9 { get; set; }
public EsToken TokenLeft10 { get; set; }
public EsToken TokenRight1 { get; set; }
public EsToken TokenRight2 { get; set; }
public EsToken TokenRight3 { get; set; }
public EsToken TokenRight4 { get; set; }
public EsToken TokenRight5 { get; set; }
public EsToken TokenRight6 { get; set; }
public EsToken TokenRight7 { get; set; }
public EsToken TokenRight8 { get; set; }
public EsToken TokenRight9 { get; set; }
public EsToken TokenRight10 { get; set; }
}
@@ -0,0 +1,22 @@
using System;
namespace Rsdo.Concordancer.Infrastructure.Search.Model;
public class EsTerm
{
public Guid Id { get; set; }
public int Frequency { get; set; }
public EsToken Token { get; set; }
public EsToken TokenRight1 { get; set; }
public EsToken TokenRight2 { get; set; }
public EsToken TokenRight3 { get; set; }
public EsToken TokenRight4 { get; set; }
public decimal Weight { get; set; }
}
@@ -0,0 +1,21 @@
using OpenSearch.Client;
namespace Rsdo.Concordancer.Infrastructure.Search.Model;
public class EsToken
{
[Keyword]
public string Form { get; set; }
[Keyword]
public string FormLower { get; set; }
[Keyword]
public string Lemma { get; set; }
[Keyword]
public string LemmaLower { get; set; }
[Keyword]
public string Msd { get; set; }
}
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Search.Queries.Concordances;
using Rsdo.Concordancer.Infrastructure.Extensions;
namespace Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
public class ConcordancesQueryBuilder : IQueryBuilder<ConcordancesQuery>
{
public QueryContainer Build(ConcordancesQuery query)
{
// Get filter queries
var queries = GetFilterQueries(query);
// Main word query
queries.Add(GetMainWordEsQuery(query.MainWord));
// Words in context queries
if (!query.WordsInContext.IsNullOrEmpty())
{
queries.AddRange(query.WordsInContext.Select(GetWordInContextEsQuery));
}
// Merge queries
var mergedQuery = queries.ToBooleanAndQuery();
// Check if we should return random rows (used in export)
if (query.ReturnRandomRows)
{
mergedQuery = new FunctionScoreQuery()
{
Query = mergedQuery,
Functions = new List<IScoreFunction>()
{
new RandomScoreFunction()
{
Seed = DateTime.Now.Ticks,
},
},
};
}
return mergedQuery;
}
private static List<QueryContainer> GetFilterQueries(ConcordancesQuery query)
{
var queries = new List<QueryContainer>();
if (!query.TextIds.IsNullOrEmpty())
{
queries.Add(
new TermsQuery()
{
Field = "textId",
Terms = query.TextIds.Cast<object>(),
});
}
return queries;
}
private static QueryContainer GetMainWordEsQuery(SearchedMainWordQuery mainWordQuery)
{
return GetWordEsQuery(mainWordQuery, GetTokenField(0));
}
private static QueryContainer GetWordEsQuery(SearchedWordQuery wordQuery, string tokenField)
{
var queries = new List<QueryContainer>();
if (!string.IsNullOrEmpty(wordQuery.Form))
{
queries.Add(
new TermQuery()
{
Field = $"{tokenField}.formLower",
Value = wordQuery.Form.ToLower(),
});
}
if (!wordQuery.Lemmas.IsNullOrEmpty())
{
queries.Add(
new TermsQuery()
{
Field = $"{tokenField}.lemma",
Terms = wordQuery.Lemmas,
});
}
if (queries.Count == 0)
{
return new MatchNoneQuery();
}
if (!wordQuery.Msds.IsNullOrEmpty())
{
queries.Add(
new TermsQuery()
{
Field = $"{tokenField}.msd",
Terms = wordQuery.Msds,
});
}
return queries.ToBooleanAndQuery();
}
private static QueryContainer GetWordInContextEsQuery(SearchedWordInContextQuery searchedWord)
{
var queries = new List<QueryContainer>();
foreach (var position in searchedWord.Positions)
{
var positionQuery = GetWordEsQuery(searchedWord, GetTokenField(position));
queries.Add(positionQuery);
}
var query = queries.ToBooleanOrQuery();
return searchedWord.ConditionType == ServiceModel.Types.ConditionType.Is ? query : !query;
}
private static string GetTokenField(int position)
{
return position switch
{
< 0 => $"tokenLeft{-position}",
> 0 => $"tokenRight{position}",
_ => "token",
};
}
}
@@ -0,0 +1,10 @@
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Search.Queries;
namespace Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
public interface IQueryBuilder<TQuery>
where TQuery : Query
{
QueryContainer Build(TQuery query);
}
@@ -0,0 +1,9 @@
using Rsdo.Concordancer.Core.Search.Queries;
namespace Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
public interface IQueryBuilderFactory
{
IQueryBuilder<TQuery> GetBuilder<TQuery>()
where TQuery : Query;
}
@@ -0,0 +1,20 @@
using Autofac;
using Rsdo.Concordancer.Core.Search.Queries;
namespace Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
public class QueryBuilderFactory : IQueryBuilderFactory
{
private readonly ILifetimeScope lifetimeScope;
public QueryBuilderFactory(ILifetimeScope lifetimeScope)
{
this.lifetimeScope = lifetimeScope;
}
public IQueryBuilder<TQuery> GetBuilder<TQuery>()
where TQuery : Query
{
return lifetimeScope.Resolve<IQueryBuilder<TQuery>>();
}
}
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using OpenSearch.Client;
using Rsdo.Concordancer.Core.Extensions;
using Rsdo.Concordancer.Core.Search.Queries.TermLists;
using Rsdo.Concordancer.Infrastructure.Extensions;
namespace Rsdo.Concordancer.Infrastructure.Search.QueryBuilders;
public class TermListQueryBuilder : IQueryBuilder<TermListQuery>
{
public QueryContainer Build(TermListQuery query)
{
var queries = new List<QueryContainer>();
// Search for words in all possible positions
for (var i = 0; i <= 5 - query.Words.Count; i++)
{
var termQuery = new List<QueryContainer>();
for (var j = 0; j < query.Words.Count; j++)
{
var word = query.Words[j];
var tokenField = GetTokenField(i + j);
termQuery.Add(GetWordEsQuery(word, tokenField));
}
queries.Add(termQuery.ToBooleanAndQuery());
}
return queries.ToBooleanOrQuery();
}
private static QueryContainer GetWordEsQuery(SearchedTermQuery wordQuery, string tokenField)
{
var queries = new List<QueryContainer>();
if (!string.IsNullOrEmpty(wordQuery.Form))
{
queries.Add(
new TermQuery()
{
Field = $"{tokenField}.formLower",
Value = wordQuery.Form.ToLower(),
});
}
if (!wordQuery.Lemmas.IsNullOrEmpty())
{
queries.Add(
new TermsQuery()
{
Field = $"{tokenField}.lemma",
Terms = wordQuery.Lemmas,
});
}
return queries.Count == 0 ? new MatchNoneQuery() : queries.ToBooleanAndQuery();
}
private static string GetTokenField(int position)
{
return position switch
{
> 0 => $"tokenRight{position}",
_ => "token",
};
}
}
@@ -0,0 +1,8 @@
using System;
namespace Rsdo.Concordancer.ServiceModel.Interfaces;
public interface IHaveCorpusId
{
public Guid CorpusId { get; set; }
}
@@ -0,0 +1,8 @@
namespace Rsdo.Concordancer.ServiceModel.Interfaces;
public interface IPagedResponse
{
int Offset { get; }
long Total { get; }
}
@@ -0,0 +1,8 @@
namespace Rsdo.Concordancer.ServiceModel.Interfaces;
public interface IPagedSearch
{
int From { get; }
int Size { get; }
}

Some files were not shown because too many files have changed in this diff Show More