A standalone full-text search engine written in C#.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

308 lines
7.6 KiB

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using SBRL.Utilities;
using LibSearchBox;
namespace SearchBoxCLI
{
enum OperatingModes
{
Query,
Index,
Add,
Remove,
Update,
GenerateContext
}
enum OutputModes
{
Json,
Text,
Html
}
class MainClass {
private static List<string> Extras = new List<string>();
private static OperatingModes Mode = OperatingModes.Query;
private static OutputModes OutputMode = OutputModes.Text;
private static bool Batch = false;
private static string Name = string.Empty;
private static IEnumerable<string> Tags;
private static string SearchIndexFilepath = string.Empty;
private static TextReader Source = Console.In;
private static TextReader SourceOld = null, SourceNew = null;
private static string Query = string.Empty;
private static int ResultsLimit = -1;
private static int ResultsOffset = 0;
public static int Main(string[] args)
{
for (int i = 0; i < args.Length; i++)
{
if (!args[i].StartsWith("-")) {
Extras.Add(args[i]);
continue;
}
switch (args[i].TrimStart("-".ToCharArray())) {
case "s":
case "source":
string sourceFilename = args[++i];
Source = new StreamReader(sourceFilename);
Name = Name.Length > 0 ? Name : sourceFilename;
break;
case "batch":
Batch = true;
break;
case "old-source":
SourceOld = new StreamReader(args[++i]);
break;
case "new-source":
string newSourceFilename = args[++i];
SourceNew = new StreamReader(newSourceFilename);
Name = Name.Length > 0 ? Name : newSourceFilename;
break;
case "tags":
Tags = Regex.Split(args[++i], @",\s*");
break;
case "n":
case "name":
Name = args[++i];
break;
case "index":
SearchIndexFilepath = args[++i];
break;
case "limit":
ResultsLimit = int.Parse(args[++i]);
break;
case "offset":
ResultsOffset = int.Parse(args[++i]);
break;
case "query":
Query = args[++i];
break;
case "format":
OutputMode = (OutputModes)Enum.Parse(typeof(OutputModes), args[++i], true);
break;
case "help":
return HandleHelp();
default:
Console.Error.WriteLine($"Error: Unknown property {args[i]}.");
return 1;
}
}
if (Extras.Count < 1) return HandleHelp();
string modeText = Extras.First().Replace("context", "generatecontext"); Extras.RemoveAt(0);
Mode = (OperatingModes)Enum.Parse(typeof(OperatingModes), modeText, true);
switch (Mode) {
case OperatingModes.Index: return HandleIndex();
case OperatingModes.Add: return HandleAdd();
case OperatingModes.Remove: return HandleRemove();
case OperatingModes.Query: return HandleQuery();
case OperatingModes.GenerateContext: return HandleContextGeneration();
default:
Console.Error.WriteLine($"Error: Don't know how to handle mode {Mode}.");
return 128;
}
}
private static int HandleHelp()
{
Console.WriteLine(EmbeddedFiles.ReadAllText("SearchBoxCLI.EmbeddedFiles.Help.txt"));
return 1;
}
private static int HandleAdd()
{
if (Name == string.Empty && !Batch) {
Console.Error.WriteLine("Error: The document name must be specified when reading from stdin!");
return 1;
}
if (SearchIndexFilepath == string.Empty) {
Console.Error.WriteLine("Error: No search index file path specified.");
return 1;
}
// --------------------------------------
SearchBox searchBox;
if (!File.Exists(SearchIndexFilepath))
searchBox = new SearchBox();
else
searchBox = JsonConvert.DeserializeObject<SearchBox>(File.ReadAllText(SearchIndexFilepath));
if (!Batch)
searchBox.AddDocument(Name, Tags, Source.ReadToEnd());
else {
try
{
Parallel.ForEach(LineIterator.GetLines(Source), (string nextLine) => {
string[] parts = nextLine.Split('|');
if (parts[0].Trim().Length == 0)
return;
searchBox.AddDocument(
parts[1].Trim(),
Regex.Split(parts[2], @",\s*"),
File.ReadAllText(parts[0].Trim())
);
Console.Error.WriteLine($"[Searchbox] [add] {parts[0].Trim()}");
});
} catch (FileNotFoundException error) {
Console.Error.WriteLine(error.Message);
return 1;
}
}
File.WriteAllText(SearchIndexFilepath, JsonConvert.SerializeObject(searchBox));
Console.Error.WriteLine($"[Searchbox] [save] {Name} -> {SearchIndexFilepath}");
return 0;
}
private static int HandleRemove()
{
if (string.IsNullOrEmpty(Name)) {
Console.Error.WriteLine("Error: The document name must be specified when removing a document!");
return 1;
}
// --------------------------------------
SearchBox searchBox = JsonConvert.DeserializeObject<SearchBox>(
File.ReadAllText(SearchIndexFilepath)
);
searchBox.RemoveDocument(Name);
File.WriteAllText(SearchIndexFilepath, JsonConvert.SerializeObject(searchBox));
Console.Error.WriteLine($"[Searchbox] [remove] {Name} <- {SearchIndexFilepath}");
return 0;
}
private static int HandleQuery()
{
if (string.IsNullOrEmpty(Query)) {
Console.Error.WriteLine("Error: No query specified!");
return 1;
}
if (SearchIndexFilepath == string.Empty) {
Console.Error.WriteLine("Error: No search index file path specified.");
return 1;
}
// Use the first line of stdin instead of the actual query string if "-" is specified
if (Query == "-") {
Query = Console.ReadLine().Trim();
}
SearchBox searchBox = JsonConvert.DeserializeObject<SearchBox>(
File.ReadAllText(SearchIndexFilepath)
);
IEnumerable<SearchResult> resultsRaw = searchBox.Query(Query, new QuerySettings()).Skip(ResultsOffset);
List<SearchResult> results = new List<SearchResult>(
ResultsLimit > 0 ? resultsRaw.Take(ResultsLimit) : resultsRaw
);
switch (OutputMode)
{
case OutputModes.Json:
Console.WriteLine(JsonConvert.SerializeObject(results));
break;
case OutputModes.Text:
int i = 0;
foreach (SearchResult nextResult in results) {
Console.WriteLine($"#{i}: {nextResult}");
i++;
}
break;
}
return 0;
}
private static int HandleContextGeneration()
{
if (string.IsNullOrEmpty(Name)) {
Console.Error.WriteLine("Error: No document name specified.");
return 1;
}
if (string.IsNullOrEmpty(Query)) {
Console.Error.WriteLine("Error: No query specified.");
return 1;
}
if (SearchIndexFilepath == string.Empty) {
Console.Error.WriteLine("Error: No search index file path specified.");
return 1;
}
SearchBox searchBox = JsonConvert.DeserializeObject<SearchBox>(
File.ReadAllText(SearchIndexFilepath)
);
ContextSettings generationSettings = new ContextSettings();
switch (OutputMode) {
case OutputModes.Json:
Console.Error.WriteLine("Error: JSON output for context generation is not supported.");
return 1;
case OutputModes.Html:
generationSettings.Html = true;
break;
case OutputModes.Text:
generationSettings.Html = false;
break;
}
Console.WriteLine(searchBox.GenerateContext(Name, Source.ReadToEnd(), Query, generationSettings));
return 0;
}
private static int HandleIndex()
{
Index index = new Index(Source.ReadToEnd());
switch (OutputMode)
{
case OutputModes.Json:
Console.WriteLine(JsonConvert.SerializeObject(index));
break;
case OutputModes.Text:
Console.WriteLine(index);
break;
}
return 0;
}
}
}