From 8da68bf90a164b4da548958b90e46f34eccafb18 Mon Sep 17 00:00:00 2001 From: amiralirahimii Date: Sat, 10 Aug 2024 13:02:46 +0330 Subject: [PATCH 1/5] init: inital commit of web-api --- 06-web-api/full-text-search.sln | 34 +++ 06-web-api/src/App/App.csproj | 41 ++++ 06-web-api/src/App/Interfaces/IInputParser.cs | 8 + 06-web-api/src/App/Program.cs | 37 +++ 06-web-api/src/App/Services/FileLoader.cs | 30 +++ 06-web-api/src/App/UI/UserInterface.cs | 69 ++++++ 06-web-api/src/App/Utilities/InputParser.cs | 40 ++++ 06-web-api/src/App/Utilities/Logging.cs | 17 ++ .../App/assets/Documents/AddYourDocsHere.txt | 1 + .../src/App/assets/Resources.Designer.cs | 71 ++++++ 06-web-api/src/App/assets/Resources.resx | 24 ++ .../DocumentManagement.csproj | 10 + .../Interfaces/IFileReader.cs | 8 + .../Interfaces/IInvertedIndex.cs | 9 + .../Interfaces/IInvertedIndexBuilder.cs | 6 + .../Interfaces/ISearchStrategy.cs | 8 + .../Interfaces/ISearcher.cs | 7 + .../Interfaces/ITokenizer.cs | 8 + .../Models/AdvancedInvertedIndex.cs | 90 ++++++++ .../Models/FileCollection.cs | 21 ++ .../Models/InvertedIndex.cs | 26 +++ .../src/DocumentManagement/Models/Keyword.cs | 11 + .../DocumentManagement/Models/KeywordInfo.cs | 3 + .../DocumentManagement/Models/SearchQuery.cs | 5 + .../Services/Files/FileReader.cs | 25 ++ .../FilesAdvancedInvertedIndexBuilder.cs | 41 ++++ .../FilesInvertedIndexBuilder.cs | 40 ++++ .../InvertedIndex/InvertedIndexSearcher.cs | 20 ++ .../ExcludedSearchStrategy.cs | 16 ++ .../MandatorySearchStrategy.cs | 16 ++ .../OptionalSearchStrategy.cs | 22 ++ .../DocumentManagement/Utilities/Tokenizer.cs | 16 ++ .../test/App/Utilities/InputParserTest.cs | 213 ++++++++++++++++++ .../Models/AdvancedInvertedIndexTest.cs | 47 ++++ .../Services/Files/FileReaderTest.cs | 69 ++++++ .../FilesAdvancedInvertedIndexBuilderTest.cs | 47 ++++ .../FilesInvertedIndexBuilderTest.cs | 47 ++++ .../InvertedIndexSearcherTest.cs | 104 +++++++++ .../Utilities/TokenizerTest.cs | 29 +++ 06-web-api/test/test.csproj | 33 +++ 40 files changed, 1369 insertions(+) create mode 100644 06-web-api/full-text-search.sln create mode 100644 06-web-api/src/App/App.csproj create mode 100644 06-web-api/src/App/Interfaces/IInputParser.cs create mode 100644 06-web-api/src/App/Program.cs create mode 100644 06-web-api/src/App/Services/FileLoader.cs create mode 100644 06-web-api/src/App/UI/UserInterface.cs create mode 100644 06-web-api/src/App/Utilities/InputParser.cs create mode 100644 06-web-api/src/App/Utilities/Logging.cs create mode 100644 06-web-api/src/App/assets/Documents/AddYourDocsHere.txt create mode 100644 06-web-api/src/App/assets/Resources.Designer.cs create mode 100644 06-web-api/src/App/assets/Resources.resx create mode 100644 06-web-api/src/DocumentManagement/DocumentManagement.csproj create mode 100644 06-web-api/src/DocumentManagement/Interfaces/IFileReader.cs create mode 100644 06-web-api/src/DocumentManagement/Interfaces/IInvertedIndex.cs create mode 100644 06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs create mode 100644 06-web-api/src/DocumentManagement/Interfaces/ISearchStrategy.cs create mode 100644 06-web-api/src/DocumentManagement/Interfaces/ISearcher.cs create mode 100644 06-web-api/src/DocumentManagement/Interfaces/ITokenizer.cs create mode 100644 06-web-api/src/DocumentManagement/Models/AdvancedInvertedIndex.cs create mode 100644 06-web-api/src/DocumentManagement/Models/FileCollection.cs create mode 100644 06-web-api/src/DocumentManagement/Models/InvertedIndex.cs create mode 100644 06-web-api/src/DocumentManagement/Models/Keyword.cs create mode 100644 06-web-api/src/DocumentManagement/Models/KeywordInfo.cs create mode 100644 06-web-api/src/DocumentManagement/Models/SearchQuery.cs create mode 100644 06-web-api/src/DocumentManagement/Services/Files/FileReader.cs create mode 100644 06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs create mode 100644 06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs create mode 100644 06-web-api/src/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcher.cs create mode 100644 06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/ExcludedSearchStrategy.cs create mode 100644 06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/MandatorySearchStrategy.cs create mode 100644 06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/OptionalSearchStrategy.cs create mode 100644 06-web-api/src/DocumentManagement/Utilities/Tokenizer.cs create mode 100644 06-web-api/test/App/Utilities/InputParserTest.cs create mode 100644 06-web-api/test/DocumentManagement/Models/AdvancedInvertedIndexTest.cs create mode 100644 06-web-api/test/DocumentManagement/Services/Files/FileReaderTest.cs create mode 100644 06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilderTest.cs create mode 100644 06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilderTest.cs create mode 100644 06-web-api/test/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcherTest.cs create mode 100644 06-web-api/test/DocumentManagement/Utilities/TokenizerTest.cs create mode 100644 06-web-api/test/test.csproj diff --git a/06-web-api/full-text-search.sln b/06-web-api/full-text-search.sln new file mode 100644 index 0000000..f73edf5 --- /dev/null +++ b/06-web-api/full-text-search.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentManagement", "src\DocumentManagement\DocumentManagement.csproj", "{D471FD38-26BA-4DEF-96A2-982F235AEA01}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{0735FD70-626C-4D88-94E3-4B410C5A41E9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test.csproj", "{FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Release|Any CPU.Build.0 = Release|Any CPU + {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Release|Any CPU.Build.0 = Release|Any CPU + {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/06-web-api/src/App/App.csproj b/06-web-api/src/App/App.csproj new file mode 100644 index 0000000..3c6dd81 --- /dev/null +++ b/06-web-api/src/App/App.csproj @@ -0,0 +1,41 @@ + + + + + + + + Mohaymen.FullTextSearch.App + Exe + net8.0 + enable + enable + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + PreserveNewest + + + + + + True + True + Resources.resx + + + + diff --git a/06-web-api/src/App/Interfaces/IInputParser.cs b/06-web-api/src/App/Interfaces/IInputParser.cs new file mode 100644 index 0000000..3048f0f --- /dev/null +++ b/06-web-api/src/App/Interfaces/IInputParser.cs @@ -0,0 +1,8 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.App.Interfaces; + +public interface IInputParser +{ + List ParseToSearchQuery(string input); +} \ No newline at end of file diff --git a/06-web-api/src/App/Program.cs b/06-web-api/src/App/Program.cs new file mode 100644 index 0000000..8be3ecc --- /dev/null +++ b/06-web-api/src/App/Program.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Logging; +using Mohaymen.FullTextSearch.App.Utilities; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; +using Mohaymen.FullTextSearch.App.Services; +using Mohaymen.FullTextSearch.App.UI; +using Mohaymen.FullTextSearch.Assets; +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Services.FilesService; +using Mohaymen.FullTextSearch.DocumentManagement.Utilities; + +namespace Mohaymen.FullTextSearch.App; + +internal class Program +{ + public static void Main() + { + var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); + var fileLoader = new FileLoader(new FileReader()); + var fileCollection = fileLoader.LoadFiles(documentsPath); + var invertedIndex = IndexFiles(fileCollection); + var invertedIndexSearcher = new InvertedIndexSearcher(invertedIndex); + var parser = new InputParser(); + var userInterface = new UserInterface(invertedIndexSearcher, parser); + userInterface.StartProgramLoop(); + } + + private static IInvertedIndex IndexFiles(FileCollection fileCollection) + { + Logging.Logger.LogInformation("Processing files..."); + var tokenizer = new Tokenizer(); + var advancedInvertedIndexBuilder = new FilesAdvancedInvertedIndexBuilder(tokenizer); + var invertedIndex = advancedInvertedIndexBuilder.IndexFilesWords(fileCollection).Build(); + Logging.Logger.LogInformation("{fileCount} files loaded.", fileCollection.FilesCount()); + return invertedIndex; + } +} \ No newline at end of file diff --git a/06-web-api/src/App/Services/FileLoader.cs b/06-web-api/src/App/Services/FileLoader.cs new file mode 100644 index 0000000..2e0df31 --- /dev/null +++ b/06-web-api/src/App/Services/FileLoader.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.App.Utilities; +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +namespace Mohaymen.FullTextSearch.App.Services; + +public class FileLoader +{ + private IFileReader _fileReader; + + public FileLoader(IFileReader fileReader) + { + _fileReader = fileReader; + } + + public FileCollection LoadFiles(string documentsPath) + { + try + { + var fileCollection = _fileReader.ReadAllFiles(documentsPath); + return fileCollection; + } + catch (DirectoryNotFoundException exception) + { + Logging.Logger.LogError(exception, "Wrong Folder Path: {path}", documentsPath); + throw; + } + } +} \ No newline at end of file diff --git a/06-web-api/src/App/UI/UserInterface.cs b/06-web-api/src/App/UI/UserInterface.cs new file mode 100644 index 0000000..7603d9e --- /dev/null +++ b/06-web-api/src/App/UI/UserInterface.cs @@ -0,0 +1,69 @@ +using Mohaymen.FullTextSearch.App.Interfaces; +using Mohaymen.FullTextSearch.Assets; +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + + +namespace Mohaymen.FullTextSearch.App.UI; + +public class UserInterface +{ + private readonly ISearcher _searcher; + private readonly IInputParser _parser; + private const string QuitCommand = "!q"; + + public UserInterface(ISearcher searcher, IInputParser parser) + { + ArgumentNullException.ThrowIfNull(searcher); + ArgumentNullException.ThrowIfNull(parser); + _searcher = searcher; + _parser = parser; + } + + public void StartProgramLoop() + { + while (true) + { + var input = GetInput(); + + if (input == QuitCommand) + break; + + var containingFiles = GetContainingFiles(input); + + DisplayResult(containingFiles); + } + } + + private ICollection GetContainingFiles(string input) + { + var searchQueries = _parser.ParseToSearchQuery(input); + return _searcher.Search(searchQueries); + } + + private void DisplayResult(ICollection containingFiles) + { + if (containingFiles.Count == 0) + { + Console.WriteLine("No result for your statement"); + return; + } + + var count = containingFiles.Count; + Console.WriteLine($"Word found in {count} file{(count > 1 ? "s" : "")}"); + Console.WriteLine("----------------------"); + foreach (var filePath in containingFiles) + { + var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); + var relativePath = Path.GetRelativePath(documentsPath, filePath); + Console.WriteLine($"File '{relativePath}'"); + }; + Console.WriteLine("----------------------"); + } + + private string GetInput() + { + Console.Write("Enter your statement (Enter !q to exit): "); + var input = Console.ReadLine()?.Trim() ?? ""; + return input; + } +} \ No newline at end of file diff --git a/06-web-api/src/App/Utilities/InputParser.cs b/06-web-api/src/App/Utilities/InputParser.cs new file mode 100644 index 0000000..3a790e0 --- /dev/null +++ b/06-web-api/src/App/Utilities/InputParser.cs @@ -0,0 +1,40 @@ +using System.Text.RegularExpressions; +using Mohaymen.FullTextSearch.App.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; + +namespace Mohaymen.FullTextSearch.App.Utilities; + +public class InputParser : IInputParser +{ + // SplitRegexPattern matches single words and phrases + private const string SplitRegexPattern = @"[+-]?\b\w+\b|[+-]?""[^""]+"""; + public List ParseToSearchQuery(string input) + { + var mandatoryWords = new List(); + var optionalWords = new List(); + var excludedWords = new List(); + + var regex = new Regex(SplitRegexPattern); + + var matches = regex.Matches(input); + + foreach (Match match in matches) + { + var word = match.Value; + + if (word.StartsWith('+')) + optionalWords.Add(new Keyword(word.Substring(1).Trim('"'))); + else if (word.StartsWith('-')) + excludedWords.Add(new Keyword(word.Substring(1).Trim('"'))); + else + mandatoryWords.Add(new Keyword(word.Trim('"'))); + } + + return [ + new SearchQuery(new MandatorySearchStrategy(), mandatoryWords), + new SearchQuery(new OptionalSearchStrategy(), optionalWords), + new SearchQuery(new ExcludedSearchStrategy(), excludedWords) + ]; + } +} diff --git a/06-web-api/src/App/Utilities/Logging.cs b/06-web-api/src/App/Utilities/Logging.cs new file mode 100644 index 0000000..bb70c45 --- /dev/null +++ b/06-web-api/src/App/Utilities/Logging.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging; + +namespace Mohaymen.FullTextSearch.App.Utilities; + +public static class Logging +{ + public static ILogger Logger{get; private set;} + + static Logging() + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }); + Logger = loggerFactory.CreateLogger(); + } +} \ No newline at end of file diff --git a/06-web-api/src/App/assets/Documents/AddYourDocsHere.txt b/06-web-api/src/App/assets/Documents/AddYourDocsHere.txt new file mode 100644 index 0000000..206f126 --- /dev/null +++ b/06-web-api/src/App/assets/Documents/AddYourDocsHere.txt @@ -0,0 +1 @@ +add your docs to this folder \ No newline at end of file diff --git a/06-web-api/src/App/assets/Resources.Designer.cs b/06-web-api/src/App/assets/Resources.Designer.cs new file mode 100644 index 0000000..934d377 --- /dev/null +++ b/06-web-api/src/App/assets/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Mohaymen.FullTextSearch.Assets { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Mohaymen.FullTextSearch.App.Assets.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Assets\Documents. + /// + internal static string DocumentsPath { + get { + return ResourceManager.GetString("DocumentsPath", resourceCulture); + } + } + } +} diff --git a/06-web-api/src/App/assets/Resources.resx b/06-web-api/src/App/assets/Resources.resx new file mode 100644 index 0000000..cc1e80e --- /dev/null +++ b/06-web-api/src/App/assets/Resources.resx @@ -0,0 +1,24 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Assets\Documents + + \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/DocumentManagement.csproj b/06-web-api/src/DocumentManagement/DocumentManagement.csproj new file mode 100644 index 0000000..3eee87d --- /dev/null +++ b/06-web-api/src/DocumentManagement/DocumentManagement.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + enable + enable + Mohaymen.FullTextSearch.DocumentManagement + + + diff --git a/06-web-api/src/DocumentManagement/Interfaces/IFileReader.cs b/06-web-api/src/DocumentManagement/Interfaces/IFileReader.cs new file mode 100644 index 0000000..6e03327 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Interfaces/IFileReader.cs @@ -0,0 +1,8 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +public interface IFileReader +{ + FileCollection ReadAllFiles(string folderPath); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndex.cs b/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndex.cs new file mode 100644 index 0000000..10df553 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndex.cs @@ -0,0 +1,9 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +public interface IInvertedIndex +{ + HashSet AllDocuments { get; } + HashSet GetDocumentsByKeyword(Keyword keyword); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs b/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs new file mode 100644 index 0000000..5f8c789 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs @@ -0,0 +1,6 @@ +namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +public interface IInvertedIndexBuilder +{ + IInvertedIndex Build(); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Interfaces/ISearchStrategy.cs b/06-web-api/src/DocumentManagement/Interfaces/ISearchStrategy.cs new file mode 100644 index 0000000..5c05e85 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Interfaces/ISearchStrategy.cs @@ -0,0 +1,8 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +public interface ISearchStrategy +{ + void FilterDocuments(HashSet documents, List keywords, IInvertedIndex invertedIndex); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Interfaces/ISearcher.cs b/06-web-api/src/DocumentManagement/Interfaces/ISearcher.cs new file mode 100644 index 0000000..7f4f87a --- /dev/null +++ b/06-web-api/src/DocumentManagement/Interfaces/ISearcher.cs @@ -0,0 +1,7 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; +namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +public interface ISearcher +{ + ICollection Search(List query); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Interfaces/ITokenizer.cs b/06-web-api/src/DocumentManagement/Interfaces/ITokenizer.cs new file mode 100644 index 0000000..493476c --- /dev/null +++ b/06-web-api/src/DocumentManagement/Interfaces/ITokenizer.cs @@ -0,0 +1,8 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +public interface ITokenizer +{ + List ExtractKeywords(string text); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Models/AdvancedInvertedIndex.cs b/06-web-api/src/DocumentManagement/Models/AdvancedInvertedIndex.cs new file mode 100644 index 0000000..b48cd46 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Models/AdvancedInvertedIndex.cs @@ -0,0 +1,90 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Models; + +public class AdvancedInvertedIndex : IInvertedIndex, IEquatable +{ + private readonly Dictionary> _invertedIndexMap = []; + public HashSet AllDocuments { get; } = []; + private ITokenizer _tokenizer; + + public AdvancedInvertedIndex(ITokenizer tokenizer) + { + _tokenizer = tokenizer; + } + + public void AddDocumentToKeyword(Keyword keyword, KeywordInfo keywordInfo) + { + AllDocuments.Add(keywordInfo.Document); + + if (!_invertedIndexMap.ContainsKey(keyword)) + _invertedIndexMap.Add(keyword, []); + + _invertedIndexMap[keyword].Add(keywordInfo); + } + public HashSet GetDocumentsByKeyword(Keyword phrase) + { + var phraseWords = _tokenizer.ExtractKeywords(phrase.Word); + + if (!phraseWords.Any()) + { + return []; + } + + foreach (var phraseWord in phraseWords) + { + if (!_invertedIndexMap.ContainsKey(phraseWord)) + { + return []; + } + } + + var keywordInfos = new HashSet(_invertedIndexMap[phraseWords[0]]); + + for (int i=1; i(_invertedIndexMap[phraseWords[i]]); + currentKeywordInfos.RemoveWhere(keywordInfo => + !keywordInfos.Contains(new KeywordInfo(keywordInfo.Document, keywordInfo.Position - 1)) + ); + + keywordInfos = currentKeywordInfos; + } + + return keywordInfos.Select(keywordInfo => keywordInfo.Document).ToHashSet(); + } + + + public bool Equals(AdvancedInvertedIndex? other) + { + if (other is null) return false; + + if (_invertedIndexMap.Count != other._invertedIndexMap.Count) + return false; + + foreach (var kvp in _invertedIndexMap) + { + if (!other._invertedIndexMap.TryGetValue(kvp.Key, out var otherSet)) + return false; + + if (!kvp.Value.SetEquals(otherSet)) + return false; + } + + var areDocumentsEqual = AllDocuments.SetEquals(other.AllDocuments); + return areDocumentsEqual; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((AdvancedInvertedIndex)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(_invertedIndexMap, AllDocuments); + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Models/FileCollection.cs b/06-web-api/src/DocumentManagement/Models/FileCollection.cs new file mode 100644 index 0000000..1c69390 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Models/FileCollection.cs @@ -0,0 +1,21 @@ +namespace Mohaymen.FullTextSearch.DocumentManagement.Models; + +public class FileCollection +{ + private readonly Dictionary _filesDataDictionary = []; + + public void AddFile(string filePath, string fileContent) => + _filesDataDictionary.Add(filePath, fileContent); + + public List GetFilesPath() => + _filesDataDictionary.Keys.ToList(); + + public string GetFileContent(string filePath) => + _filesDataDictionary[filePath]; + + public bool ContainsFile(string filePath) => + _filesDataDictionary.ContainsKey(filePath); + + public int FilesCount() => + _filesDataDictionary.Count; +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Models/InvertedIndex.cs b/06-web-api/src/DocumentManagement/Models/InvertedIndex.cs new file mode 100644 index 0000000..e8d5aaf --- /dev/null +++ b/06-web-api/src/DocumentManagement/Models/InvertedIndex.cs @@ -0,0 +1,26 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Models; + +public class InvertedIndex : IInvertedIndex +{ + private readonly Dictionary> _invertedIndexMap = []; + public HashSet AllDocuments { get; } = []; + + public void AddDocumentToKeyword(Keyword keyword, string document) + { + AllDocuments.Add(document); + + if (!_invertedIndexMap.ContainsKey(keyword)) + _invertedIndexMap.Add(keyword, []); + + _invertedIndexMap[keyword].Add(document); + } + + public HashSet GetDocumentsByKeyword(Keyword keyword) + { + _invertedIndexMap.TryGetValue(keyword, out HashSet? documents); + + return documents ?? []; + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Models/Keyword.cs b/06-web-api/src/DocumentManagement/Models/Keyword.cs new file mode 100644 index 0000000..3a7cb96 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Models/Keyword.cs @@ -0,0 +1,11 @@ +namespace Mohaymen.FullTextSearch.DocumentManagement.Models; + +public record Keyword +{ + public Keyword(string word) + { + Word = word.ToUpper(); + } + + public string Word { get; init; } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Models/KeywordInfo.cs b/06-web-api/src/DocumentManagement/Models/KeywordInfo.cs new file mode 100644 index 0000000..5cad16b --- /dev/null +++ b/06-web-api/src/DocumentManagement/Models/KeywordInfo.cs @@ -0,0 +1,3 @@ +namespace Mohaymen.FullTextSearch.DocumentManagement.Models; + +public record KeywordInfo(string Document, int Position); \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Models/SearchQuery.cs b/06-web-api/src/DocumentManagement/Models/SearchQuery.cs new file mode 100644 index 0000000..c95b864 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Models/SearchQuery.cs @@ -0,0 +1,5 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Models; + +public record SearchQuery(ISearchStrategy SearchStrategy, List Keywords); \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/Files/FileReader.cs b/06-web-api/src/DocumentManagement/Services/Files/FileReader.cs new file mode 100644 index 0000000..4d076df --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/Files/FileReader.cs @@ -0,0 +1,25 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.FilesService; + +public class FileReader : IFileReader +{ + public FileCollection ReadAllFiles(string folderPath) + { + var files = Directory.GetFiles(folderPath); + var fileCollection = files.Aggregate(new FileCollection(), AddFileToCollection); + + return fileCollection; + } + + private FileCollection AddFileToCollection(FileCollection collection, string filePath) + { + if (!collection.ContainsFile(filePath)) + collection.AddFile( + filePath, + File.ReadAllText(filePath) + ); + + return collection; + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs new file mode 100644 index 0000000..af6f85e --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs @@ -0,0 +1,41 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; + +public class FilesAdvancedInvertedIndexBuilder : IInvertedIndexBuilder +{ + private AdvancedInvertedIndex _advancedInvertedIndex; + private ITokenizer _tokenizer; + + public FilesAdvancedInvertedIndexBuilder(ITokenizer tokenizer) + { + _advancedInvertedIndex = new(tokenizer); + _tokenizer = tokenizer; + } + + public FilesAdvancedInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection) + { + foreach (var filePath in fileCollection.GetFilesPath()) + { + var keywords = _tokenizer.ExtractKeywords(fileCollection.GetFileContent(filePath)); + UpdateInvertedIndexMap(keywords, filePath); + } + + return this; + } + + private void UpdateInvertedIndexMap(List keywords, string filePath) + { + for (var i = 0; i < keywords.Count; i++) + { + _advancedInvertedIndex.AddDocumentToKeyword(keywords[i], new KeywordInfo(filePath, i)); + } + } + + + public IInvertedIndex Build() + { + return _advancedInvertedIndex; + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs new file mode 100644 index 0000000..c9313eb --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs @@ -0,0 +1,40 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Utilities; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; + +public class FilesInvertedIndexBuilder : IInvertedIndexBuilder +{ + private readonly InvertedIndex _invertedIndex = new(); + private readonly ITokenizer _tokenizer; + + public FilesInvertedIndexBuilder(ITokenizer tokenizer) + { + _tokenizer = tokenizer; + } + + public FilesInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection) + { + foreach (var filePath in fileCollection.GetFilesPath()) + { + var keywords = _tokenizer.ExtractKeywords(fileCollection.GetFileContent(filePath)); + UpdateInvertedIndexMap(keywords, filePath); + } + + return this; + } + + private void UpdateInvertedIndexMap(List keywords, string filePath) + { + foreach (var keyword in keywords) + { + _invertedIndex.AddDocumentToKeyword(keyword, filePath); + } + } + + public IInvertedIndex Build() + { + return _invertedIndex; + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcher.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcher.cs new file mode 100644 index 0000000..c44c25c --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcher.cs @@ -0,0 +1,20 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; + +public class InvertedIndexSearcher(IInvertedIndex invertedIndex) : ISearcher +{ + public ICollection Search(List queries) + { + var filteredDocuments = new HashSet(invertedIndex.AllDocuments); + + foreach (var (searchStrategy, keywords) in queries) + { + searchStrategy.FilterDocuments(filteredDocuments, keywords, invertedIndex); + } + + return filteredDocuments; + } + +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/ExcludedSearchStrategy.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/ExcludedSearchStrategy.cs new file mode 100644 index 0000000..9550676 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/ExcludedSearchStrategy.cs @@ -0,0 +1,16 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; + +public class ExcludedSearchStrategy : ISearchStrategy +{ + public void FilterDocuments(HashSet documents, List keywords, IInvertedIndex invertedIndex) + { + foreach (var keyword in keywords) + { + HashSet currentFiles = invertedIndex.GetDocumentsByKeyword(keyword); + documents.ExceptWith(currentFiles); + } + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/MandatorySearchStrategy.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/MandatorySearchStrategy.cs new file mode 100644 index 0000000..5354c8a --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/MandatorySearchStrategy.cs @@ -0,0 +1,16 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; + +public class MandatorySearchStrategy : ISearchStrategy +{ + public void FilterDocuments(HashSet documents, List keywords, IInvertedIndex invertedIndex) + { + foreach (var keyword in keywords) + { + HashSet currentFiles = invertedIndex.GetDocumentsByKeyword(keyword); + documents.IntersectWith(currentFiles); + } + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/OptionalSearchStrategy.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/OptionalSearchStrategy.cs new file mode 100644 index 0000000..f34d067 --- /dev/null +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/SearchStrategies/OptionalSearchStrategy.cs @@ -0,0 +1,22 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; + +public class OptionalSearchStrategy : ISearchStrategy +{ + public void FilterDocuments(HashSet documents, List keywords, IInvertedIndex invertedIndex) + { + var optionalsSet = new HashSet(); + foreach (var keyword in keywords) + { + HashSet currentFiles = invertedIndex.GetDocumentsByKeyword(keyword); + optionalsSet.UnionWith(currentFiles); + } + + if (keywords.Count > 0) + { + documents.IntersectWith(optionalsSet); + } + } +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Utilities/Tokenizer.cs b/06-web-api/src/DocumentManagement/Utilities/Tokenizer.cs new file mode 100644 index 0000000..19e2e9b --- /dev/null +++ b/06-web-api/src/DocumentManagement/Utilities/Tokenizer.cs @@ -0,0 +1,16 @@ +using System.Text.RegularExpressions; +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; + +namespace Mohaymen.FullTextSearch.DocumentManagement.Utilities; + +public class Tokenizer(string splitRegex = @"[^\w']+") : ITokenizer +{ + public List ExtractKeywords(string text) + { + return Regex.Split(text, splitRegex) + .Where(word => !string.IsNullOrWhiteSpace(word)) + .Select(word => new Keyword(word)) + .ToList(); + } +} \ No newline at end of file diff --git a/06-web-api/test/App/Utilities/InputParserTest.cs b/06-web-api/test/App/Utilities/InputParserTest.cs new file mode 100644 index 0000000..382dc4c --- /dev/null +++ b/06-web-api/test/App/Utilities/InputParserTest.cs @@ -0,0 +1,213 @@ +using Mohaymen.FullTextSearch.App.Utilities; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; + +namespace Mohaymen.FullTextSearch.Test.App.Utilities; + +public class InputParserTest +{ + private readonly InputParser _inputParser = new(); + + public static IEnumerable ParseToSearchQuery_ShouldCategorizeMixedInputCorrectly_Data() + { + yield return + [ + "", + new List { }, + new List { }, + new List { }, + ]; + + yield return + [ + "hamed dooset -darim", + new List { new("hamed"), new("dooset") }, + new List { }, + new List { new("darim") }, + ]; + + yield return + [ + "-arshad -arshada ali", + new List { new("ali") }, + new List { }, + new List { new("arshad"), new("arshada") }, + ]; + + yield return + [ + "+amirali -khosh amadi", + new List { new("amadi") }, + new List { new("amirali") }, + new List { new("khosh") }, + ]; + + yield return + [ + "+keyword1 +keyword2 -keyword3 keyword4", + new List { new("keyword4") }, + new List { new("keyword1"), new("keyword2") }, + new List { new("keyword3") }, + ]; + + yield return + [ + "+optional -excluded", + new List(), + new List { new("optional") }, + new List { new("excluded") }, + ]; + + yield return + [ + "+OPT1 -excl1 MAN1 MAN2 +OPT2 -excl2", + new List { new("MAN1"), new("MAN2") }, + new List { new("OPT1"), new("OPT2") }, + new List { new("EXCL1"), new("EXCL2") }, + ]; + + yield return + [ + "+optional1 +optional2 +optional3 -excluded1 -excluded2 mandatory1 mandatory2", + new List { new("MANDATORY1"), new("MANDATORY2") }, + new List { new("OPTIONAL1"), new("OPTIONAL2"), new("OPTIONAL3") }, + new List { new("EXCLUDED1"), new("EXCLUDED2") }, + ]; + + yield return + [ + "+OPtional1 -exCluDed1 ManDatory1", + new List { new("MANDATORY1") }, + new List { new("OPTIONAL1") }, + new List { new("EXCLUDED1") }, + ]; + + yield return + [ + "+optional1 -excluded1 mandatory1 +optional2 -excluded2 mandatory2", + new List { new("MANDATORY1"), new("MANDATORY2") }, + new List { new("OPTIONAL1"), new("OPTIONAL2") }, + new List { new("EXCLUDED1"), new("EXCLUDED2") }, + ]; + + yield return + [ + "+optional word1 -word2 word3 word4 -word5 +word6", + new List { new("WORD1"), new("WORD3"), new("WORD4") }, + new List { new("OPTIONAL"), new("WORD6") }, + new List { new("WORD2"), new("WORD5") }, + ]; + } + + [Theory] + [MemberData(nameof(ParseToSearchQuery_ShouldCategorizeMixedInputCorrectly_Data))] + public void ParseToSearchQuery_ShouldCategorizeMixedInputCorrectly( + string input, List expectedMandatories, List expectedOptionals, + List expectedExcludeds) + { + // Act + var queries = _inputParser.ParseToSearchQuery(input); + + // Assert + var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; + var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; + var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; + + Assert.Equal(expectedMandatories, mandatories); + Assert.Equal(expectedExcludeds, excludeds); + Assert.Equal(expectedOptionals, optionals); + } + + + [Fact] + public void ParseToSearchQuery_ShouldCategorizeMandatoryCorrectly() + { + // Arrange + var input = "ahaghsenad emah ravras firahs"; + List expectedMandatories = + [ + new("ahaghsenad"), new("emah"), new("ravras"), new("firahs") + ]; + + // Act + var queries = _inputParser.ParseToSearchQuery(input); + + // Assert + var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; + var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; + var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; + + Assert.Equal(expectedMandatories, mandatories); + Assert.Empty(optionals); + Assert.Empty(excludeds); + } + + [Fact] + public void ParseToSearchQuery_ShouldCategorizeOptionalCorrectly() + { + // Arrange + var input = "+reverse +input +of +mandatory +test"; + List expectedOptionals = + [ + new("reverse"), new("input"), new("of"), new("mandatory"), new("test") + ]; + + // Act + var queries = _inputParser.ParseToSearchQuery(input); + + // Assert + var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; + var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; + var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; + + Assert.Equal(expectedOptionals, optionals); + Assert.Empty(mandatories); + Assert.Empty(excludeds); + } + + [Fact] + public void ParseToSearchQuery_ShouldCategorizeExcludedCorrectly() + { + // Arrange + var input = "-find -the -easter -egg"; + List expectedExcludeds = + [ + new("find"), new("the"), new("easter"), new("egg") + ]; + + // Act + var queries = _inputParser.ParseToSearchQuery(input); + + // Assert + var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; + var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; + var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; + + Assert.Equal(expectedExcludeds, excludeds); + Assert.Empty(mandatories); + Assert.Empty(optionals); + } + + [Fact] + public void ParseToSearchQuery_ShouldHandlePhrases() + { + // Arrange + var input = "+\"word1 word2\" -\"word3\" \"word4 word5 word6\""; + List expectedMandatories = [new Keyword("word4 word5 word6")]; + List expectedOptionals = [new Keyword("word1 word2")]; + List expectedExcludeds = [new Keyword("word3")]; + + // Assert + var queries = _inputParser.ParseToSearchQuery(input); + + // Assert + var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; + var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; + var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; + + Assert.Equal(expectedMandatories, mandatories); + Assert.Equal(expectedOptionals, optionals); + Assert.Equal(expectedExcludeds, excludeds); + } + +} \ No newline at end of file diff --git a/06-web-api/test/DocumentManagement/Models/AdvancedInvertedIndexTest.cs b/06-web-api/test/DocumentManagement/Models/AdvancedInvertedIndexTest.cs new file mode 100644 index 0000000..49733dc --- /dev/null +++ b/06-web-api/test/DocumentManagement/Models/AdvancedInvertedIndexTest.cs @@ -0,0 +1,47 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Utilities; + +namespace Mohaymen.FullTextSearch.Test.DocumentManagement.Models; + +public class AdvancedInvertedIndexTest +{ + private readonly AdvancedInvertedIndex _advancedInvertedIndex; + public AdvancedInvertedIndexTest() + { + _advancedInvertedIndex = new AdvancedInvertedIndex(new Tokenizer()); + _advancedInvertedIndex.AddDocumentToKeyword( + new Keyword("key1"), + new KeywordInfo("doc1", 1) + ); + _advancedInvertedIndex.AddDocumentToKeyword( + new Keyword("key1"), + new KeywordInfo ("doc2", 3) + ); + _advancedInvertedIndex.AddDocumentToKeyword( + new Keyword("key2"), + new KeywordInfo ("doc1", 2) + ); + _advancedInvertedIndex.AddDocumentToKeyword( + new Keyword("key2"), + new KeywordInfo ("doc2", 2) + ); + } + + [Fact] + public void GetDocGetDocumentsByKeyword_ShouldWorkCorrectlyForPhrases() + { + // Act + var documents1 = _advancedInvertedIndex.GetDocumentsByKeyword(new Keyword("key1 key2")); + var documents2 = _advancedInvertedIndex.GetDocumentsByKeyword(new Keyword("key2 key1")); + // Assert + Assert.Equal(["doc1"], documents1); + Assert.Equal(["doc2"], documents2); + } + + [Fact] + public void GetDocGetDocumentsByKeyword_ShouldWorkCorrectlyForWords() + { + var documents1 = _advancedInvertedIndex.GetDocumentsByKeyword(new Keyword("key1")); + Assert.Equal(documents1, ["doc1", "doc2"]); + } +} \ No newline at end of file diff --git a/06-web-api/test/DocumentManagement/Services/Files/FileReaderTest.cs b/06-web-api/test/DocumentManagement/Services/Files/FileReaderTest.cs new file mode 100644 index 0000000..6e1ed8e --- /dev/null +++ b/06-web-api/test/DocumentManagement/Services/Files/FileReaderTest.cs @@ -0,0 +1,69 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Services.FilesService; + +namespace Mohaymen.FullTextSearch.Test.DocumentManagement.Services.Files; + +public class FileReaderTest : IDisposable +{ + private readonly FileReader _fileReader; + private readonly string _testDirectory; + private readonly string _emptyFolderPath; + + public FileReaderTest() + { + _testDirectory = CreateTestDirectoryWithFiles(); + _emptyFolderPath = CreateEmptyDirectory(); + _fileReader = new FileReader(); + } + + private string CreateTestDirectoryWithFiles() + { + var testDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(testDirectory); + + var filePath1 = Path.Combine(testDirectory, "file1.txt"); + var filePath2 = Path.Combine(testDirectory, "file2.txt"); + File.WriteAllText(filePath1, "Content of file1"); + File.WriteAllText(filePath2, "Content of file2"); + + return testDirectory; + } + + private string CreateEmptyDirectory() + { + var emptyFolderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(emptyFolderPath); + + return emptyFolderPath; + } + + + [Fact] + public void ReadAllFiles_ShouldReadAllFilesInADirectory() + { + // Act + var result = _fileReader.ReadAllFiles(_testDirectory); + + // Assert + Assert.Equal(2, result.FilesCount()); + Assert.Contains(Path.Combine(_testDirectory, "file1.txt"), result.GetFilesPath()); + Assert.Contains(Path.Combine(_testDirectory, "file2.txt"), result.GetFilesPath()); + Assert.Equal("Content of file1", result.GetFileContent(Path.Combine(_testDirectory, "file1.txt"))); + Assert.Equal("Content of file2", result.GetFileContent(Path.Combine(_testDirectory, "file2.txt"))); + } + + [Fact] + public void ReadAllFiles_ShouldHandleEmptyDirectory() + { + // Act + var result = _fileReader.ReadAllFiles(_emptyFolderPath); + + // Assert + Assert.Equal(0, result.FilesCount()); + } + + public void Dispose() + { + if (Directory.Exists(_testDirectory)) + Directory.Delete(_testDirectory, true); + } +} \ No newline at end of file diff --git a/06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilderTest.cs b/06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilderTest.cs new file mode 100644 index 0000000..a5a40d8 --- /dev/null +++ b/06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilderTest.cs @@ -0,0 +1,47 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; +using Mohaymen.FullTextSearch.DocumentManagement.Utilities; +using NSubstitute; + +namespace Mohaymen.FullTextSearch.Test.DocumentManagement.Services.InvertedIndex; + +public class FilesAdvancedInvertedIndexBuilderTest +{ + private readonly ITokenizer _tokenizer; + private readonly FilesAdvancedInvertedIndexBuilder _filesAdvancedInvertedIndexBuilder; + + public FilesAdvancedInvertedIndexBuilderTest() + { + _tokenizer = new Tokenizer(); + _filesAdvancedInvertedIndexBuilder = new FilesAdvancedInvertedIndexBuilder(_tokenizer); + } + + [Fact] + public void IndexFilesWords_ValidFileCollection_ShouldIndexAllWords() + { + // Arrange + var fileCollection = new FileCollection(); + fileCollection.AddFile("doc1.txt", "star academy star"); + fileCollection.AddFile("doc2.txt", "star coder academy"); + fileCollection.AddFile("doc3.txt", "academy coder"); + fileCollection.AddFile("doc4.txt", "summer"); + + var expectedInvertedIndex = new AdvancedInvertedIndex(_tokenizer); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("star"), new KeywordInfo("doc1.txt", 0)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("academy"), new KeywordInfo("doc1.txt", 1)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("star"), new KeywordInfo("doc1.txt", 2)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("star"), new KeywordInfo("doc2.txt", 0)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("coder"), new KeywordInfo("doc2.txt", 1)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("academy"), new KeywordInfo("doc2.txt", 2)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("academy"), new KeywordInfo("doc3.txt", 0)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("coder"), new KeywordInfo("doc3.txt", 1)); + expectedInvertedIndex.AddDocumentToKeyword(new Keyword("summer"), new KeywordInfo("doc4.txt", 0)); + + // Act + IInvertedIndex invertedIndex = _filesAdvancedInvertedIndexBuilder.IndexFilesWords(fileCollection).Build(); + + // Assert + Assert.Equal(expectedInvertedIndex, invertedIndex); + } +} \ No newline at end of file diff --git a/06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilderTest.cs b/06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilderTest.cs new file mode 100644 index 0000000..2f41b82 --- /dev/null +++ b/06-web-api/test/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilderTest.cs @@ -0,0 +1,47 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; +using NSubstitute; + +namespace Mohaymen.FullTextSearch.Test.DocumentManagement.Services.InvertedIndex; + +public class FilesInvertedIndexBuilderTest +{ + private readonly ITokenizer _tokenizer; + private readonly FilesInvertedIndexBuilder _filesInvertedIndexBuilder; + + public FilesInvertedIndexBuilderTest() + { + _tokenizer = Substitute.For(); + _filesInvertedIndexBuilder = new FilesInvertedIndexBuilder(_tokenizer); + } + + [Fact] + public void IndexFilesWords_ValidFileCollection_ShouldIndexAllWords() + { + // Arrange + var fileCollection = new FileCollection(); + fileCollection.AddFile("doc1.txt", "star academy star"); + fileCollection.AddFile("doc2.txt", "star coder"); + fileCollection.AddFile("doc3.txt", "academy coder"); + fileCollection.AddFile("doc4.txt", "summer"); + + _tokenizer.ExtractKeywords("star academy star") + .Returns([new Keyword("star"), new Keyword("academy"), new Keyword("star")]); + _tokenizer.ExtractKeywords("star coder") + .Returns([new Keyword("star"), new Keyword("coder")]); + _tokenizer.ExtractKeywords("academy coder") + .Returns([new Keyword("academy"), new Keyword("coder")]); + _tokenizer.ExtractKeywords("summer") + .Returns([new Keyword("summer")]); + + // Act + IInvertedIndex invertedIndex = _filesInvertedIndexBuilder.IndexFilesWords(fileCollection).Build(); + + // Assert + Assert.Equal(["doc1.txt", "doc2.txt"], invertedIndex.GetDocumentsByKeyword(new Keyword("star"))); + Assert.Equal(["doc1.txt", "doc3.txt"], invertedIndex.GetDocumentsByKeyword(new Keyword("academy"))); + Assert.Equal(["doc2.txt", "doc3.txt"], invertedIndex.GetDocumentsByKeyword(new Keyword("coder"))); + Assert.Equal(["doc4.txt"], invertedIndex.GetDocumentsByKeyword(new Keyword("summer"))); + } +} \ No newline at end of file diff --git a/06-web-api/test/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcherTest.cs b/06-web-api/test/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcherTest.cs new file mode 100644 index 0000000..c5922d6 --- /dev/null +++ b/06-web-api/test/DocumentManagement/Services/InvertedIndex/InvertedIndexSearcherTest.cs @@ -0,0 +1,104 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; +using NSubstitute; + +namespace Mohaymen.FullTextSearch.Test.DocumentManagement.Services.InvertedIndex; + +public class InvertedIndexSearcherTest +{ + private IInvertedIndex _invertedIndex; + private InvertedIndexSearcher _searcher; + + [Fact] + public void Search_WithMultipleStrategies_CombinesFiltersCorrectly() + { + // Arrange + var invertedIndex = Substitute.For(); + + var keywordDocumentMapping = new Dictionary> + { + { new Keyword("star"), ["doc1.txt", "doc2.txt"] }, + { new Keyword("academy"), ["doc1.txt", "doc3.txt"] }, + { new Keyword("coder"), ["doc2.txt", "doc3.txt"] }, + { new Keyword("summer"), ["doc4.txt"] } + }; + + foreach (var entry in keywordDocumentMapping) + { + invertedIndex.GetDocumentsByKeyword(entry.Key) + .Returns(entry.Value); + } + + invertedIndex.AllDocuments.Returns(["doc1.txt", "doc2.txt", "doc3.txt", "doc4.txt"]); + _searcher = new InvertedIndexSearcher(invertedIndex); + + var mandatorySearchStrategy = new MandatorySearchStrategy(); + var optionalSearchStrategy = new OptionalSearchStrategy(); + var excludedSearchStrategy = new ExcludedSearchStrategy(); + var mandatoryKeywords = new List { new Keyword("academy") }; + var optionalKeywords = new List { new Keyword("coder") }; + var excludedKeywords = new List { new Keyword("star") }; + + var queries = new List + { + new SearchQuery(mandatorySearchStrategy, mandatoryKeywords), + new SearchQuery(optionalSearchStrategy, optionalKeywords), + new SearchQuery(excludedSearchStrategy, excludedKeywords) + }; + + // Act + var results = _searcher.Search(queries); + + // Assert + var expected = new HashSet { "doc3.txt" }; + Assert.True(expected.SetEquals(results), $"Expected: {string.Join(", ", expected)}, Actual: {string.Join(", ", results)}"); + } + + [Fact] + public void Search_WithMultipleStrategies_CombinesFiltersCorrectly_When() + { + // Arrange + var invertedIndex = Substitute.For(); + + var keywordDocumentMapping = new Dictionary> + { + { new Keyword("star academy"), ["doc1.txt", "doc2.txt"] }, + { new Keyword("academy"), ["doc1.txt", "doc2.txt", "doc3.txt"] }, + { new Keyword("star"), ["doc1.txt", "doc2.txt", "doc4.txt"] }, + { new Keyword("coder"), ["doc2.txt", "doc3.txt"] }, + { new Keyword("summer"), ["doc1.txt", "doc2.txt" ,"doc4.txt"] } + }; + + foreach (var entry in keywordDocumentMapping) + { + invertedIndex.GetDocumentsByKeyword(entry.Key) + .Returns(entry.Value); + } + + invertedIndex.AllDocuments.Returns(["doc1.txt", "doc2.txt", "doc3.txt", "doc4.txt"]); + _searcher = new InvertedIndexSearcher(invertedIndex); + + var mandatorySearchStrategy = new MandatorySearchStrategy(); + var optionalSearchStrategy = new OptionalSearchStrategy(); + var excludedSearchStrategy = new ExcludedSearchStrategy(); + var mandatoryKeywords = new List { new Keyword("star academy") }; + var optionalKeywords = new List { new Keyword("summer") }; + var excludedKeywords = new List { new Keyword("coder") }; + + var queries = new List + { + new SearchQuery(mandatorySearchStrategy, mandatoryKeywords), + new SearchQuery(optionalSearchStrategy, optionalKeywords), + new SearchQuery(excludedSearchStrategy, excludedKeywords) + }; + + // Act + var results = _searcher.Search(queries); + + // Assert + var expected = new HashSet { "doc1.txt" }; + Assert.True(expected.SetEquals(results), $"Expected: {string.Join(", ", expected)}, Actual: {string.Join(", ", results)}"); + } +} \ No newline at end of file diff --git a/06-web-api/test/DocumentManagement/Utilities/TokenizerTest.cs b/06-web-api/test/DocumentManagement/Utilities/TokenizerTest.cs new file mode 100644 index 0000000..d833023 --- /dev/null +++ b/06-web-api/test/DocumentManagement/Utilities/TokenizerTest.cs @@ -0,0 +1,29 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Utilities; + +namespace Mohaymen.FullTextSearch.Test.DocumentManagement.Utilities; + +public class TokenizersTests +{ + public static IEnumerable GetTestData() + { + yield return ["sta'r", new List { new ("sta'r") }]; + yield return ["'star", new List { new ("'star") }]; + yield return ["st 'ar", new List { new ("st"), new ("'ar") }]; + yield return ["st' ar", new List { new("st'"), new("ar") }]; + } + + [Theory] + [MemberData(nameof(GetTestData))] + public void ExtractKeywords_ShouldTokenizeText_WhenStringContainsSingleQuotationAndSpace(string text, List expectedKeywords) + { + //Arrange + var tokenizer = new Tokenizer(); + + //Act + var keywords = tokenizer.ExtractKeywords(text); + + //Assert + Assert.Equal(expectedKeywords, keywords); + } +} \ No newline at end of file diff --git a/06-web-api/test/test.csproj b/06-web-api/test/test.csproj new file mode 100644 index 0000000..24f58ca --- /dev/null +++ b/06-web-api/test/test.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + Mohaymen.FullTextSearch.Test + false + true + + + + + + + + + + + + + + + + + + + + + + + + From f988b6e9f633dbd79c8f97772afdf9089dae9858 Mon Sep 17 00:00:00 2001 From: amiralirahimii Date: Sat, 10 Aug 2024 13:57:22 +0330 Subject: [PATCH 2/5] feat: add web-api feature --- .../Controllers/InvertedIndexController.cs | 29 ++++++++++ .../MyWebApplication/Helpers/QueryObject.cs | 8 +++ .../MyWebApplication/MyWebApplication.csproj | 19 +++++++ 06-web-api/MyWebApplication/Program.cs | 43 ++++++++++++++ .../Properties/launchSettings.json | 41 ++++++++++++++ .../appsettings.Development.json | 8 +++ 06-web-api/MyWebApplication/appsettings.json | 9 +++ 06-web-api/full-text-search.sln | 6 ++ 06-web-api/src/App/ApplicationService.cs | 56 +++++++++++++++++++ .../src/App/Interfaces/IApplicationService.cs | 6 ++ .../Interfaces/IInvertedIndexBuilder.cs | 3 + .../FilesAdvancedInvertedIndexBuilder.cs | 2 +- .../FilesInvertedIndexBuilder.cs | 2 +- 13 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs create mode 100644 06-web-api/MyWebApplication/Helpers/QueryObject.cs create mode 100644 06-web-api/MyWebApplication/MyWebApplication.csproj create mode 100644 06-web-api/MyWebApplication/Program.cs create mode 100644 06-web-api/MyWebApplication/Properties/launchSettings.json create mode 100644 06-web-api/MyWebApplication/appsettings.Development.json create mode 100644 06-web-api/MyWebApplication/appsettings.json create mode 100644 06-web-api/src/App/ApplicationService.cs create mode 100644 06-web-api/src/App/Interfaces/IApplicationService.cs diff --git a/06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs b/06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs new file mode 100644 index 0000000..0d792c8 --- /dev/null +++ b/06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using Mohaymen.FullTextSearch.App; +using Mohaymen.FullTextSearch.App.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using MyWebApplication.Helpers; + +namespace MyWebApplication.Controllers; + +[ApiController] +[Route("[controller]")] +public class InvertedIndexController : ControllerBase +{ + private readonly IApplicationService _applicationService; + public InvertedIndexController(IApplicationService applicationService) + { + _applicationService = applicationService; + } + [HttpGet] + public IActionResult GetAll([FromQuery]QueryObject queryObject) + { + return Ok( + _applicationService.Search( + queryObject.MandatoryWords, + queryObject.OptionalWords, + queryObject.ExcludedWords + ) + ); + } +} \ No newline at end of file diff --git a/06-web-api/MyWebApplication/Helpers/QueryObject.cs b/06-web-api/MyWebApplication/Helpers/QueryObject.cs new file mode 100644 index 0000000..cdb40e4 --- /dev/null +++ b/06-web-api/MyWebApplication/Helpers/QueryObject.cs @@ -0,0 +1,8 @@ +namespace MyWebApplication.Helpers; + +public class QueryObject +{ + public List MandatoryWords { get; set; } = new(); + public List ExcludedWords { get; set; } = new(); + public List OptionalWords { get; set; } = new(); +} \ No newline at end of file diff --git a/06-web-api/MyWebApplication/MyWebApplication.csproj b/06-web-api/MyWebApplication/MyWebApplication.csproj new file mode 100644 index 0000000..19e1041 --- /dev/null +++ b/06-web-api/MyWebApplication/MyWebApplication.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/06-web-api/MyWebApplication/Program.cs b/06-web-api/MyWebApplication/Program.cs new file mode 100644 index 0000000..ec206c5 --- /dev/null +++ b/06-web-api/MyWebApplication/Program.cs @@ -0,0 +1,43 @@ +using Mohaymen.FullTextSearch.App; +using Mohaymen.FullTextSearch.App.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Services.FilesService; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; +using Mohaymen.FullTextSearch.DocumentManagement.Utilities; + +namespace MyWebApplication; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddAuthorization(); + builder.Services.AddControllers(); + + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.MapControllers(); + app.UseHttpsRedirection(); + app.UseAuthorization(); + + app.Run(); + } +} \ No newline at end of file diff --git a/06-web-api/MyWebApplication/Properties/launchSettings.json b/06-web-api/MyWebApplication/Properties/launchSettings.json new file mode 100644 index 0000000..93ed998 --- /dev/null +++ b/06-web-api/MyWebApplication/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:18722", + "sslPort": 44326 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5113", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7100;http://localhost:5113", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/06-web-api/MyWebApplication/appsettings.Development.json b/06-web-api/MyWebApplication/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/06-web-api/MyWebApplication/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/06-web-api/MyWebApplication/appsettings.json b/06-web-api/MyWebApplication/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/06-web-api/MyWebApplication/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/06-web-api/full-text-search.sln b/06-web-api/full-text-search.sln index f73edf5..9c5b6e7 100644 --- a/06-web-api/full-text-search.sln +++ b/06-web-api/full-text-search.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test.csproj", "{FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyWebApplication", "MyWebApplication\MyWebApplication.csproj", "{FD4AE171-A581-4FD2-899C-C1BB5EAFE641}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Release|Any CPU.Build.0 = Release|Any CPU + {FD4AE171-A581-4FD2-899C-C1BB5EAFE641}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD4AE171-A581-4FD2-899C-C1BB5EAFE641}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD4AE171-A581-4FD2-899C-C1BB5EAFE641}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD4AE171-A581-4FD2-899C-C1BB5EAFE641}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/06-web-api/src/App/ApplicationService.cs b/06-web-api/src/App/ApplicationService.cs new file mode 100644 index 0000000..9a885ed --- /dev/null +++ b/06-web-api/src/App/ApplicationService.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Logging; +using Mohaymen.FullTextSearch.App.Interfaces; +using Mohaymen.FullTextSearch.App.Services; +using Mohaymen.FullTextSearch.App.Utilities; +using Mohaymen.FullTextSearch.Assets; +using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; +using Mohaymen.FullTextSearch.DocumentManagement.Models; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; +using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; + +namespace Mohaymen.FullTextSearch.App; + +public class ApplicationService : IApplicationService +{ + private readonly IInvertedIndexBuilder _invertedIndexBuilder; + private readonly ISearcher _invertedIndexSearcher; + public ApplicationService(IFileReader fileReader, IInvertedIndexBuilder invertedIndexBuilder) + { + var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); + var fileLoader = new FileLoader(fileReader); + var fileCollection = fileLoader.LoadFiles(documentsPath); + _invertedIndexBuilder = invertedIndexBuilder; + var invertedIndex = IndexFiles(fileCollection); + _invertedIndexSearcher = new InvertedIndexSearcher(invertedIndex); + } + + private IInvertedIndex IndexFiles(FileCollection fileCollection) + { + Logging.Logger.LogInformation("Processing files..."); + var invertedIndex = _invertedIndexBuilder.IndexFilesWords(fileCollection).Build(); + Logging.Logger.LogInformation("{fileCount} files loaded.", fileCollection.FilesCount()); + return invertedIndex; + } + + public IEnumerable Search(List mandatoryWords, List optionalWords, List excludedWords) + { + var query = new List + { + new SearchQuery( + new MandatorySearchStrategy(), + mandatoryWords.Select(word => new Keyword(word)).ToList() + ), + new SearchQuery( + new OptionalSearchStrategy(), + optionalWords.Select(word => new Keyword(word)).ToList() + ), + new SearchQuery( + new ExcludedSearchStrategy(), + excludedWords.Select(word => new Keyword(word)).ToList() + ) + }; + return _invertedIndexSearcher + .Search(query) + .Select(fullPath => Path.GetFileName(fullPath)); + } +} \ No newline at end of file diff --git a/06-web-api/src/App/Interfaces/IApplicationService.cs b/06-web-api/src/App/Interfaces/IApplicationService.cs new file mode 100644 index 0000000..6e2292d --- /dev/null +++ b/06-web-api/src/App/Interfaces/IApplicationService.cs @@ -0,0 +1,6 @@ +namespace Mohaymen.FullTextSearch.App.Interfaces; + +public interface IApplicationService +{ + IEnumerable Search(List mandatoryWords, List optionalWords, List excludedWords); +} \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs b/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs index 5f8c789..b2c08af 100644 --- a/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs +++ b/06-web-api/src/DocumentManagement/Interfaces/IInvertedIndexBuilder.cs @@ -1,6 +1,9 @@ +using Mohaymen.FullTextSearch.DocumentManagement.Models; + namespace Mohaymen.FullTextSearch.DocumentManagement.Interfaces; public interface IInvertedIndexBuilder { IInvertedIndex Build(); + IInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection); } \ No newline at end of file diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs index af6f85e..01f67c5 100644 --- a/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesAdvancedInvertedIndexBuilder.cs @@ -14,7 +14,7 @@ public FilesAdvancedInvertedIndexBuilder(ITokenizer tokenizer) _tokenizer = tokenizer; } - public FilesAdvancedInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection) + public IInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection) { foreach (var filePath in fileCollection.GetFilesPath()) { diff --git a/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs index c9313eb..7c54efa 100644 --- a/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs +++ b/06-web-api/src/DocumentManagement/Services/InvertedIndex/FilesInvertedIndexBuilder.cs @@ -14,7 +14,7 @@ public FilesInvertedIndexBuilder(ITokenizer tokenizer) _tokenizer = tokenizer; } - public FilesInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection) + public IInvertedIndexBuilder IndexFilesWords(FileCollection fileCollection) { foreach (var filePath in fileCollection.GetFilesPath()) { From c24ad8cb4999a2e794448946ab3ff67b28ab863c Mon Sep 17 00:00:00 2001 From: Mobin Barfi Date: Sat, 10 Aug 2024 16:58:00 +0330 Subject: [PATCH 3/5] refactor: reconstruct web application structure --- 06-web-api/full-text-search.sln | 8 +-- 06-web-api/src/App/App.csproj | 41 ----------- 06-web-api/src/App/Interfaces/IInputParser.cs | 8 --- 06-web-api/src/App/Program.cs | 37 ---------- 06-web-api/src/App/Services/FileLoader.cs | 30 -------- 06-web-api/src/App/UI/UserInterface.cs | 69 ------------------- 06-web-api/src/App/Utilities/InputParser.cs | 40 ----------- 06-web-api/src/App/Utilities/Logging.cs | 17 ----- .../Controllers/InvertedIndexController.cs | 8 +-- .../MyWebApplication/Helpers/QueryObject.cs | 2 +- .../Interfaces/IApplicationService.cs | 2 +- .../MyWebApplication/MyWebApplication.csproj | 11 ++- .../{ => src}/MyWebApplication/Program.cs | 6 +- .../Properties/launchSettings.json | 0 .../Services}/ApplicationService.cs | 20 +++--- .../appsettings.Development.json | 0 .../MyWebApplication/appsettings.json | 0 .../assets/Documents/AddYourDocsHere.txt | 0 .../assets/Resources.Designer.cs | 4 +- .../assets/Resources.resx | 2 +- 06-web-api/test/test.csproj | 1 - 21 files changed, 30 insertions(+), 276 deletions(-) delete mode 100644 06-web-api/src/App/App.csproj delete mode 100644 06-web-api/src/App/Interfaces/IInputParser.cs delete mode 100644 06-web-api/src/App/Program.cs delete mode 100644 06-web-api/src/App/Services/FileLoader.cs delete mode 100644 06-web-api/src/App/UI/UserInterface.cs delete mode 100644 06-web-api/src/App/Utilities/InputParser.cs delete mode 100644 06-web-api/src/App/Utilities/Logging.cs rename 06-web-api/{ => src}/MyWebApplication/Controllers/InvertedIndexController.cs (74%) rename 06-web-api/{ => src}/MyWebApplication/Helpers/QueryObject.cs (77%) rename 06-web-api/src/{App => MyWebApplication}/Interfaces/IApplicationService.cs (70%) rename 06-web-api/{ => src}/MyWebApplication/MyWebApplication.csproj (53%) rename 06-web-api/{ => src}/MyWebApplication/Program.cs (89%) rename 06-web-api/{ => src}/MyWebApplication/Properties/launchSettings.json (100%) rename 06-web-api/src/{App => MyWebApplication/Services}/ApplicationService.cs (76%) rename 06-web-api/{ => src}/MyWebApplication/appsettings.Development.json (100%) rename 06-web-api/{ => src}/MyWebApplication/appsettings.json (100%) rename 06-web-api/src/{App => MyWebApplication}/assets/Documents/AddYourDocsHere.txt (100%) rename 06-web-api/src/{App => MyWebApplication}/assets/Resources.Designer.cs (95%) rename 06-web-api/src/{App => MyWebApplication}/assets/Resources.resx (95%) diff --git a/06-web-api/full-text-search.sln b/06-web-api/full-text-search.sln index 9c5b6e7..2afceb5 100644 --- a/06-web-api/full-text-search.sln +++ b/06-web-api/full-text-search.sln @@ -5,11 +5,9 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentManagement", "src\DocumentManagement\DocumentManagement.csproj", "{D471FD38-26BA-4DEF-96A2-982F235AEA01}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{0735FD70-626C-4D88-94E3-4B410C5A41E9}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test.csproj", "{FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyWebApplication", "MyWebApplication\MyWebApplication.csproj", "{FD4AE171-A581-4FD2-899C-C1BB5EAFE641}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyWebApplication", "src\MyWebApplication\MyWebApplication.csproj", "{FD4AE171-A581-4FD2-899C-C1BB5EAFE641}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,10 +19,6 @@ Global {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Debug|Any CPU.Build.0 = Debug|Any CPU {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Release|Any CPU.ActiveCfg = Release|Any CPU {D471FD38-26BA-4DEF-96A2-982F235AEA01}.Release|Any CPU.Build.0 = Release|Any CPU - {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0735FD70-626C-4D88-94E3-4B410C5A41E9}.Release|Any CPU.Build.0 = Release|Any CPU {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD776EED-DBA5-4789-A1A7-FF97EF11FBD3}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/06-web-api/src/App/App.csproj b/06-web-api/src/App/App.csproj deleted file mode 100644 index 3c6dd81..0000000 --- a/06-web-api/src/App/App.csproj +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - Mohaymen.FullTextSearch.App - Exe - net8.0 - enable - enable - - - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - PreserveNewest - - - - - - True - True - Resources.resx - - - - diff --git a/06-web-api/src/App/Interfaces/IInputParser.cs b/06-web-api/src/App/Interfaces/IInputParser.cs deleted file mode 100644 index 3048f0f..0000000 --- a/06-web-api/src/App/Interfaces/IInputParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Mohaymen.FullTextSearch.DocumentManagement.Models; - -namespace Mohaymen.FullTextSearch.App.Interfaces; - -public interface IInputParser -{ - List ParseToSearchQuery(string input); -} \ No newline at end of file diff --git a/06-web-api/src/App/Program.cs b/06-web-api/src/App/Program.cs deleted file mode 100644 index 8be3ecc..0000000 --- a/06-web-api/src/App/Program.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.Extensions.Logging; -using Mohaymen.FullTextSearch.App.Utilities; -using Mohaymen.FullTextSearch.DocumentManagement.Models; -using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; -using Mohaymen.FullTextSearch.App.Services; -using Mohaymen.FullTextSearch.App.UI; -using Mohaymen.FullTextSearch.Assets; -using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; -using Mohaymen.FullTextSearch.DocumentManagement.Services.FilesService; -using Mohaymen.FullTextSearch.DocumentManagement.Utilities; - -namespace Mohaymen.FullTextSearch.App; - -internal class Program -{ - public static void Main() - { - var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); - var fileLoader = new FileLoader(new FileReader()); - var fileCollection = fileLoader.LoadFiles(documentsPath); - var invertedIndex = IndexFiles(fileCollection); - var invertedIndexSearcher = new InvertedIndexSearcher(invertedIndex); - var parser = new InputParser(); - var userInterface = new UserInterface(invertedIndexSearcher, parser); - userInterface.StartProgramLoop(); - } - - private static IInvertedIndex IndexFiles(FileCollection fileCollection) - { - Logging.Logger.LogInformation("Processing files..."); - var tokenizer = new Tokenizer(); - var advancedInvertedIndexBuilder = new FilesAdvancedInvertedIndexBuilder(tokenizer); - var invertedIndex = advancedInvertedIndexBuilder.IndexFilesWords(fileCollection).Build(); - Logging.Logger.LogInformation("{fileCount} files loaded.", fileCollection.FilesCount()); - return invertedIndex; - } -} \ No newline at end of file diff --git a/06-web-api/src/App/Services/FileLoader.cs b/06-web-api/src/App/Services/FileLoader.cs deleted file mode 100644 index 2e0df31..0000000 --- a/06-web-api/src/App/Services/FileLoader.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.Extensions.Logging; -using Mohaymen.FullTextSearch.DocumentManagement.Models; -using Mohaymen.FullTextSearch.App.Utilities; -using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; - -namespace Mohaymen.FullTextSearch.App.Services; - -public class FileLoader -{ - private IFileReader _fileReader; - - public FileLoader(IFileReader fileReader) - { - _fileReader = fileReader; - } - - public FileCollection LoadFiles(string documentsPath) - { - try - { - var fileCollection = _fileReader.ReadAllFiles(documentsPath); - return fileCollection; - } - catch (DirectoryNotFoundException exception) - { - Logging.Logger.LogError(exception, "Wrong Folder Path: {path}", documentsPath); - throw; - } - } -} \ No newline at end of file diff --git a/06-web-api/src/App/UI/UserInterface.cs b/06-web-api/src/App/UI/UserInterface.cs deleted file mode 100644 index 7603d9e..0000000 --- a/06-web-api/src/App/UI/UserInterface.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Mohaymen.FullTextSearch.App.Interfaces; -using Mohaymen.FullTextSearch.Assets; -using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; - - -namespace Mohaymen.FullTextSearch.App.UI; - -public class UserInterface -{ - private readonly ISearcher _searcher; - private readonly IInputParser _parser; - private const string QuitCommand = "!q"; - - public UserInterface(ISearcher searcher, IInputParser parser) - { - ArgumentNullException.ThrowIfNull(searcher); - ArgumentNullException.ThrowIfNull(parser); - _searcher = searcher; - _parser = parser; - } - - public void StartProgramLoop() - { - while (true) - { - var input = GetInput(); - - if (input == QuitCommand) - break; - - var containingFiles = GetContainingFiles(input); - - DisplayResult(containingFiles); - } - } - - private ICollection GetContainingFiles(string input) - { - var searchQueries = _parser.ParseToSearchQuery(input); - return _searcher.Search(searchQueries); - } - - private void DisplayResult(ICollection containingFiles) - { - if (containingFiles.Count == 0) - { - Console.WriteLine("No result for your statement"); - return; - } - - var count = containingFiles.Count; - Console.WriteLine($"Word found in {count} file{(count > 1 ? "s" : "")}"); - Console.WriteLine("----------------------"); - foreach (var filePath in containingFiles) - { - var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); - var relativePath = Path.GetRelativePath(documentsPath, filePath); - Console.WriteLine($"File '{relativePath}'"); - }; - Console.WriteLine("----------------------"); - } - - private string GetInput() - { - Console.Write("Enter your statement (Enter !q to exit): "); - var input = Console.ReadLine()?.Trim() ?? ""; - return input; - } -} \ No newline at end of file diff --git a/06-web-api/src/App/Utilities/InputParser.cs b/06-web-api/src/App/Utilities/InputParser.cs deleted file mode 100644 index 3a790e0..0000000 --- a/06-web-api/src/App/Utilities/InputParser.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.RegularExpressions; -using Mohaymen.FullTextSearch.App.Interfaces; -using Mohaymen.FullTextSearch.DocumentManagement.Models; -using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; - -namespace Mohaymen.FullTextSearch.App.Utilities; - -public class InputParser : IInputParser -{ - // SplitRegexPattern matches single words and phrases - private const string SplitRegexPattern = @"[+-]?\b\w+\b|[+-]?""[^""]+"""; - public List ParseToSearchQuery(string input) - { - var mandatoryWords = new List(); - var optionalWords = new List(); - var excludedWords = new List(); - - var regex = new Regex(SplitRegexPattern); - - var matches = regex.Matches(input); - - foreach (Match match in matches) - { - var word = match.Value; - - if (word.StartsWith('+')) - optionalWords.Add(new Keyword(word.Substring(1).Trim('"'))); - else if (word.StartsWith('-')) - excludedWords.Add(new Keyword(word.Substring(1).Trim('"'))); - else - mandatoryWords.Add(new Keyword(word.Trim('"'))); - } - - return [ - new SearchQuery(new MandatorySearchStrategy(), mandatoryWords), - new SearchQuery(new OptionalSearchStrategy(), optionalWords), - new SearchQuery(new ExcludedSearchStrategy(), excludedWords) - ]; - } -} diff --git a/06-web-api/src/App/Utilities/Logging.cs b/06-web-api/src/App/Utilities/Logging.cs deleted file mode 100644 index bb70c45..0000000 --- a/06-web-api/src/App/Utilities/Logging.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace Mohaymen.FullTextSearch.App.Utilities; - -public static class Logging -{ - public static ILogger Logger{get; private set;} - - static Logging() - { - using var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddConsole(); - }); - Logger = loggerFactory.CreateLogger(); - } -} \ No newline at end of file diff --git a/06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs b/06-web-api/src/MyWebApplication/Controllers/InvertedIndexController.cs similarity index 74% rename from 06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs rename to 06-web-api/src/MyWebApplication/Controllers/InvertedIndexController.cs index 0d792c8..94aeec1 100644 --- a/06-web-api/MyWebApplication/Controllers/InvertedIndexController.cs +++ b/06-web-api/src/MyWebApplication/Controllers/InvertedIndexController.cs @@ -1,10 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using Mohaymen.FullTextSearch.App; -using Mohaymen.FullTextSearch.App.Interfaces; -using Mohaymen.FullTextSearch.DocumentManagement.Models; -using MyWebApplication.Helpers; +using Mohaymen.FullTextSearch.MyWebApplication.Helpers; +using Mohaymen.FullTextSearch.MyWebApplication.Interfaces; -namespace MyWebApplication.Controllers; +namespace Mohaymen.FullTextSearch.MyWebApplication.Controllers; [ApiController] [Route("[controller]")] diff --git a/06-web-api/MyWebApplication/Helpers/QueryObject.cs b/06-web-api/src/MyWebApplication/Helpers/QueryObject.cs similarity index 77% rename from 06-web-api/MyWebApplication/Helpers/QueryObject.cs rename to 06-web-api/src/MyWebApplication/Helpers/QueryObject.cs index cdb40e4..6ed6700 100644 --- a/06-web-api/MyWebApplication/Helpers/QueryObject.cs +++ b/06-web-api/src/MyWebApplication/Helpers/QueryObject.cs @@ -1,4 +1,4 @@ -namespace MyWebApplication.Helpers; +namespace Mohaymen.FullTextSearch.MyWebApplication.Helpers; public class QueryObject { diff --git a/06-web-api/src/App/Interfaces/IApplicationService.cs b/06-web-api/src/MyWebApplication/Interfaces/IApplicationService.cs similarity index 70% rename from 06-web-api/src/App/Interfaces/IApplicationService.cs rename to 06-web-api/src/MyWebApplication/Interfaces/IApplicationService.cs index 6e2292d..5d2d542 100644 --- a/06-web-api/src/App/Interfaces/IApplicationService.cs +++ b/06-web-api/src/MyWebApplication/Interfaces/IApplicationService.cs @@ -1,4 +1,4 @@ -namespace Mohaymen.FullTextSearch.App.Interfaces; +namespace Mohaymen.FullTextSearch.MyWebApplication.Interfaces; public interface IApplicationService { diff --git a/06-web-api/MyWebApplication/MyWebApplication.csproj b/06-web-api/src/MyWebApplication/MyWebApplication.csproj similarity index 53% rename from 06-web-api/MyWebApplication/MyWebApplication.csproj rename to 06-web-api/src/MyWebApplication/MyWebApplication.csproj index 19e1041..89d28a3 100644 --- a/06-web-api/MyWebApplication/MyWebApplication.csproj +++ b/06-web-api/src/MyWebApplication/MyWebApplication.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Mohaymen.FullTextSearch.MyWebApplication @@ -12,8 +13,14 @@ - - + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + diff --git a/06-web-api/MyWebApplication/Program.cs b/06-web-api/src/MyWebApplication/Program.cs similarity index 89% rename from 06-web-api/MyWebApplication/Program.cs rename to 06-web-api/src/MyWebApplication/Program.cs index ec206c5..99879b8 100644 --- a/06-web-api/MyWebApplication/Program.cs +++ b/06-web-api/src/MyWebApplication/Program.cs @@ -1,11 +1,11 @@ -using Mohaymen.FullTextSearch.App; -using Mohaymen.FullTextSearch.App.Interfaces; using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; using Mohaymen.FullTextSearch.DocumentManagement.Services.FilesService; using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; using Mohaymen.FullTextSearch.DocumentManagement.Utilities; +using Mohaymen.FullTextSearch.MyWebApplication.Interfaces; +using Mohaymen.FullTextSearch.MyWebApplication.Services; -namespace MyWebApplication; +namespace Mohaymen.FullTextSearch.MyWebApplication; public class Program { diff --git a/06-web-api/MyWebApplication/Properties/launchSettings.json b/06-web-api/src/MyWebApplication/Properties/launchSettings.json similarity index 100% rename from 06-web-api/MyWebApplication/Properties/launchSettings.json rename to 06-web-api/src/MyWebApplication/Properties/launchSettings.json diff --git a/06-web-api/src/App/ApplicationService.cs b/06-web-api/src/MyWebApplication/Services/ApplicationService.cs similarity index 76% rename from 06-web-api/src/App/ApplicationService.cs rename to 06-web-api/src/MyWebApplication/Services/ApplicationService.cs index 9a885ed..a658f3a 100644 --- a/06-web-api/src/App/ApplicationService.cs +++ b/06-web-api/src/MyWebApplication/Services/ApplicationService.cs @@ -1,24 +1,22 @@ -using Microsoft.Extensions.Logging; -using Mohaymen.FullTextSearch.App.Interfaces; -using Mohaymen.FullTextSearch.App.Services; -using Mohaymen.FullTextSearch.App.Utilities; -using Mohaymen.FullTextSearch.Assets; +using Mohaymen.FullTextSearch.Assets; using Mohaymen.FullTextSearch.DocumentManagement.Interfaces; using Mohaymen.FullTextSearch.DocumentManagement.Models; using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService; using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; +using Mohaymen.FullTextSearch.MyWebApplication.Interfaces; -namespace Mohaymen.FullTextSearch.App; +namespace Mohaymen.FullTextSearch.MyWebApplication.Services; public class ApplicationService : IApplicationService { private readonly IInvertedIndexBuilder _invertedIndexBuilder; private readonly ISearcher _invertedIndexSearcher; - public ApplicationService(IFileReader fileReader, IInvertedIndexBuilder invertedIndexBuilder) + private readonly ILogger _logger; + public ApplicationService(IFileReader fileReader, IInvertedIndexBuilder invertedIndexBuilder, ILogger logger) { + _logger = logger; var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); - var fileLoader = new FileLoader(fileReader); - var fileCollection = fileLoader.LoadFiles(documentsPath); + var fileCollection = fileReader.ReadAllFiles(documentsPath); _invertedIndexBuilder = invertedIndexBuilder; var invertedIndex = IndexFiles(fileCollection); _invertedIndexSearcher = new InvertedIndexSearcher(invertedIndex); @@ -26,9 +24,9 @@ public ApplicationService(IFileReader fileReader, IInvertedIndexBuilder inverted private IInvertedIndex IndexFiles(FileCollection fileCollection) { - Logging.Logger.LogInformation("Processing files..."); + _logger.LogInformation("Processing files..."); var invertedIndex = _invertedIndexBuilder.IndexFilesWords(fileCollection).Build(); - Logging.Logger.LogInformation("{fileCount} files loaded.", fileCollection.FilesCount()); + _logger.LogInformation("{fileCount} files loaded.", fileCollection.FilesCount()); return invertedIndex; } diff --git a/06-web-api/MyWebApplication/appsettings.Development.json b/06-web-api/src/MyWebApplication/appsettings.Development.json similarity index 100% rename from 06-web-api/MyWebApplication/appsettings.Development.json rename to 06-web-api/src/MyWebApplication/appsettings.Development.json diff --git a/06-web-api/MyWebApplication/appsettings.json b/06-web-api/src/MyWebApplication/appsettings.json similarity index 100% rename from 06-web-api/MyWebApplication/appsettings.json rename to 06-web-api/src/MyWebApplication/appsettings.json diff --git a/06-web-api/src/App/assets/Documents/AddYourDocsHere.txt b/06-web-api/src/MyWebApplication/assets/Documents/AddYourDocsHere.txt similarity index 100% rename from 06-web-api/src/App/assets/Documents/AddYourDocsHere.txt rename to 06-web-api/src/MyWebApplication/assets/Documents/AddYourDocsHere.txt diff --git a/06-web-api/src/App/assets/Resources.Designer.cs b/06-web-api/src/MyWebApplication/assets/Resources.Designer.cs similarity index 95% rename from 06-web-api/src/App/assets/Resources.Designer.cs rename to 06-web-api/src/MyWebApplication/assets/Resources.Designer.cs index 934d377..3ed2de9 100644 --- a/06-web-api/src/App/assets/Resources.Designer.cs +++ b/06-web-api/src/MyWebApplication/assets/Resources.Designer.cs @@ -38,7 +38,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Mohaymen.FullTextSearch.App.Assets.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Mohaymen.FullTextSearch.MyWebApplication.assets.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -60,7 +60,7 @@ internal Resources() { } /// - /// Looks up a localized string similar to Assets\Documents. + /// Looks up a localized string similar to assets\Documents. /// internal static string DocumentsPath { get { diff --git a/06-web-api/src/App/assets/Resources.resx b/06-web-api/src/MyWebApplication/assets/Resources.resx similarity index 95% rename from 06-web-api/src/App/assets/Resources.resx rename to 06-web-api/src/MyWebApplication/assets/Resources.resx index cc1e80e..c221b3e 100644 --- a/06-web-api/src/App/assets/Resources.resx +++ b/06-web-api/src/MyWebApplication/assets/Resources.resx @@ -19,6 +19,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Assets\Documents + assets\Documents \ No newline at end of file diff --git a/06-web-api/test/test.csproj b/06-web-api/test/test.csproj index 24f58ca..a2941c8 100644 --- a/06-web-api/test/test.csproj +++ b/06-web-api/test/test.csproj @@ -23,7 +23,6 @@ - From 162f193e537a8c91484e8bc610df7e22485a1f01 Mon Sep 17 00:00:00 2001 From: Mohammad Mahdi Rajabi Date: Sat, 10 Aug 2024 17:05:05 +0330 Subject: [PATCH 4/5] fix: add assets to output directory --- 06-web-api/src/MyWebApplication/MyWebApplication.csproj | 7 +++++++ .../src/MyWebApplication/Services/ApplicationService.cs | 1 + 2 files changed, 8 insertions(+) diff --git a/06-web-api/src/MyWebApplication/MyWebApplication.csproj b/06-web-api/src/MyWebApplication/MyWebApplication.csproj index 89d28a3..6dd106d 100644 --- a/06-web-api/src/MyWebApplication/MyWebApplication.csproj +++ b/06-web-api/src/MyWebApplication/MyWebApplication.csproj @@ -23,4 +23,11 @@ + + + PreserveNewest + + + + diff --git a/06-web-api/src/MyWebApplication/Services/ApplicationService.cs b/06-web-api/src/MyWebApplication/Services/ApplicationService.cs index a658f3a..d8d2ed9 100644 --- a/06-web-api/src/MyWebApplication/Services/ApplicationService.cs +++ b/06-web-api/src/MyWebApplication/Services/ApplicationService.cs @@ -16,6 +16,7 @@ public ApplicationService(IFileReader fileReader, IInvertedIndexBuilder inverted { _logger = logger; var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); + Console.WriteLine(documentsPath); var fileCollection = fileReader.ReadAllFiles(documentsPath); _invertedIndexBuilder = invertedIndexBuilder; var invertedIndex = IndexFiles(fileCollection); From d41c3351361074c1a98a76bf8236d4a1f95639f6 Mon Sep 17 00:00:00 2001 From: Mobin Barfi Date: Sat, 10 Aug 2024 17:15:42 +0330 Subject: [PATCH 5/5] refactor: use AddSingleton --- 06-web-api/src/MyWebApplication/Program.cs | 8 +- .../Services/ApplicationService.cs | 1 - .../test/App/Utilities/InputParserTest.cs | 213 ------------------ 06-web-api/test/test.csproj | 4 - 4 files changed, 4 insertions(+), 222 deletions(-) delete mode 100644 06-web-api/test/App/Utilities/InputParserTest.cs diff --git a/06-web-api/src/MyWebApplication/Program.cs b/06-web-api/src/MyWebApplication/Program.cs index 99879b8..494fda7 100644 --- a/06-web-api/src/MyWebApplication/Program.cs +++ b/06-web-api/src/MyWebApplication/Program.cs @@ -20,10 +20,10 @@ public static void Main(string[] args) // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/06-web-api/src/MyWebApplication/Services/ApplicationService.cs b/06-web-api/src/MyWebApplication/Services/ApplicationService.cs index d8d2ed9..a658f3a 100644 --- a/06-web-api/src/MyWebApplication/Services/ApplicationService.cs +++ b/06-web-api/src/MyWebApplication/Services/ApplicationService.cs @@ -16,7 +16,6 @@ public ApplicationService(IFileReader fileReader, IInvertedIndexBuilder inverted { _logger = logger; var documentsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Resources.DocumentsPath); - Console.WriteLine(documentsPath); var fileCollection = fileReader.ReadAllFiles(documentsPath); _invertedIndexBuilder = invertedIndexBuilder; var invertedIndex = IndexFiles(fileCollection); diff --git a/06-web-api/test/App/Utilities/InputParserTest.cs b/06-web-api/test/App/Utilities/InputParserTest.cs deleted file mode 100644 index 382dc4c..0000000 --- a/06-web-api/test/App/Utilities/InputParserTest.cs +++ /dev/null @@ -1,213 +0,0 @@ -using Mohaymen.FullTextSearch.App.Utilities; -using Mohaymen.FullTextSearch.DocumentManagement.Models; -using Mohaymen.FullTextSearch.DocumentManagement.Services.InvertedIndexService.SearchStrategies; - -namespace Mohaymen.FullTextSearch.Test.App.Utilities; - -public class InputParserTest -{ - private readonly InputParser _inputParser = new(); - - public static IEnumerable ParseToSearchQuery_ShouldCategorizeMixedInputCorrectly_Data() - { - yield return - [ - "", - new List { }, - new List { }, - new List { }, - ]; - - yield return - [ - "hamed dooset -darim", - new List { new("hamed"), new("dooset") }, - new List { }, - new List { new("darim") }, - ]; - - yield return - [ - "-arshad -arshada ali", - new List { new("ali") }, - new List { }, - new List { new("arshad"), new("arshada") }, - ]; - - yield return - [ - "+amirali -khosh amadi", - new List { new("amadi") }, - new List { new("amirali") }, - new List { new("khosh") }, - ]; - - yield return - [ - "+keyword1 +keyword2 -keyword3 keyword4", - new List { new("keyword4") }, - new List { new("keyword1"), new("keyword2") }, - new List { new("keyword3") }, - ]; - - yield return - [ - "+optional -excluded", - new List(), - new List { new("optional") }, - new List { new("excluded") }, - ]; - - yield return - [ - "+OPT1 -excl1 MAN1 MAN2 +OPT2 -excl2", - new List { new("MAN1"), new("MAN2") }, - new List { new("OPT1"), new("OPT2") }, - new List { new("EXCL1"), new("EXCL2") }, - ]; - - yield return - [ - "+optional1 +optional2 +optional3 -excluded1 -excluded2 mandatory1 mandatory2", - new List { new("MANDATORY1"), new("MANDATORY2") }, - new List { new("OPTIONAL1"), new("OPTIONAL2"), new("OPTIONAL3") }, - new List { new("EXCLUDED1"), new("EXCLUDED2") }, - ]; - - yield return - [ - "+OPtional1 -exCluDed1 ManDatory1", - new List { new("MANDATORY1") }, - new List { new("OPTIONAL1") }, - new List { new("EXCLUDED1") }, - ]; - - yield return - [ - "+optional1 -excluded1 mandatory1 +optional2 -excluded2 mandatory2", - new List { new("MANDATORY1"), new("MANDATORY2") }, - new List { new("OPTIONAL1"), new("OPTIONAL2") }, - new List { new("EXCLUDED1"), new("EXCLUDED2") }, - ]; - - yield return - [ - "+optional word1 -word2 word3 word4 -word5 +word6", - new List { new("WORD1"), new("WORD3"), new("WORD4") }, - new List { new("OPTIONAL"), new("WORD6") }, - new List { new("WORD2"), new("WORD5") }, - ]; - } - - [Theory] - [MemberData(nameof(ParseToSearchQuery_ShouldCategorizeMixedInputCorrectly_Data))] - public void ParseToSearchQuery_ShouldCategorizeMixedInputCorrectly( - string input, List expectedMandatories, List expectedOptionals, - List expectedExcludeds) - { - // Act - var queries = _inputParser.ParseToSearchQuery(input); - - // Assert - var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; - var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; - var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; - - Assert.Equal(expectedMandatories, mandatories); - Assert.Equal(expectedExcludeds, excludeds); - Assert.Equal(expectedOptionals, optionals); - } - - - [Fact] - public void ParseToSearchQuery_ShouldCategorizeMandatoryCorrectly() - { - // Arrange - var input = "ahaghsenad emah ravras firahs"; - List expectedMandatories = - [ - new("ahaghsenad"), new("emah"), new("ravras"), new("firahs") - ]; - - // Act - var queries = _inputParser.ParseToSearchQuery(input); - - // Assert - var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; - var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; - var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; - - Assert.Equal(expectedMandatories, mandatories); - Assert.Empty(optionals); - Assert.Empty(excludeds); - } - - [Fact] - public void ParseToSearchQuery_ShouldCategorizeOptionalCorrectly() - { - // Arrange - var input = "+reverse +input +of +mandatory +test"; - List expectedOptionals = - [ - new("reverse"), new("input"), new("of"), new("mandatory"), new("test") - ]; - - // Act - var queries = _inputParser.ParseToSearchQuery(input); - - // Assert - var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; - var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; - var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; - - Assert.Equal(expectedOptionals, optionals); - Assert.Empty(mandatories); - Assert.Empty(excludeds); - } - - [Fact] - public void ParseToSearchQuery_ShouldCategorizeExcludedCorrectly() - { - // Arrange - var input = "-find -the -easter -egg"; - List expectedExcludeds = - [ - new("find"), new("the"), new("easter"), new("egg") - ]; - - // Act - var queries = _inputParser.ParseToSearchQuery(input); - - // Assert - var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; - var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; - var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; - - Assert.Equal(expectedExcludeds, excludeds); - Assert.Empty(mandatories); - Assert.Empty(optionals); - } - - [Fact] - public void ParseToSearchQuery_ShouldHandlePhrases() - { - // Arrange - var input = "+\"word1 word2\" -\"word3\" \"word4 word5 word6\""; - List expectedMandatories = [new Keyword("word4 word5 word6")]; - List expectedOptionals = [new Keyword("word1 word2")]; - List expectedExcludeds = [new Keyword("word3")]; - - // Assert - var queries = _inputParser.ParseToSearchQuery(input); - - // Assert - var mandatories = queries.Find(query => query.SearchStrategy is MandatorySearchStrategy)?.Keywords; - var optionals = queries.Find(query => query.SearchStrategy is OptionalSearchStrategy)?.Keywords; - var excludeds = queries.Find(query => query.SearchStrategy is ExcludedSearchStrategy)?.Keywords; - - Assert.Equal(expectedMandatories, mandatories); - Assert.Equal(expectedOptionals, optionals); - Assert.Equal(expectedExcludeds, excludeds); - } - -} \ No newline at end of file diff --git a/06-web-api/test/test.csproj b/06-web-api/test/test.csproj index a2941c8..34432b7 100644 --- a/06-web-api/test/test.csproj +++ b/06-web-api/test/test.csproj @@ -25,8 +25,4 @@ - - - -