|
using Nest; |
|
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Linq.Expressions; |
|
using System.Text; |
|
using System.Threading; |
|
using System.Threading.Tasks; |
|
|
|
namespace ESP.Console.Commands.Search |
|
{ |
|
//TerminalCommand and ITerminalCommand are just wrappers for our codebase to make it easier for me to run abritrary code. |
|
// you can just drop the contents into a new project with Execute as static void main. |
|
[TerminalCommand("search")] |
|
public class SearchNewCommand : ITerminalCommand |
|
{ |
|
const string index = "student"; |
|
private const string SortAnalyzerName = "sort_analyzer"; |
|
private static IndexName indexName = new IndexName() { Name = index }; |
|
|
|
public void Execute(CommandArguments args, ConnectionStrings connectionStrings) |
|
{ |
|
var client = GetClient(); |
|
|
|
RecreateIndex(client); |
|
Seed(client); |
|
|
|
Thread.Sleep(1000); //give elastic a second to get ready |
|
|
|
Sanity(client); |
|
|
|
|
|
var testCases = new List<TestCase> |
|
{ |
|
//no formating of the input |
|
new TestCase("bob", 2), // match on first name |
|
new TestCase("bob smith", 1), // exact match should return the one result that matches full string |
|
|
|
//literal string |
|
new TestCase("\"bob\"", 2), // attempt to match on partial input |
|
new TestCase("\"bob smi\"", 1), // attempt to match on partial input |
|
new TestCase("\"bob smith\"", 1), // attempt to match on full name |
|
|
|
//querystring-like fields |
|
new TestCase("bob*", 2), // wildcard match |
|
new TestCase("bob?smith", 1), // replace the space with a ? to signify "some character" |
|
new TestCase("*bob?smith*", 1), // replace space and specify "contains" |
|
new TestCase("bob\\ smith", 1), // attempt to escape the space |
|
new TestCase("*smith", 2), // |
|
new TestCase("*smith*", 2), // |
|
|
|
//combined literal with wildcards |
|
new TestCase("\"bob\"*", 2), // |
|
new TestCase("*\"bob\"*", 2), // |
|
new TestCase("*\"smith\"*", 2), // |
|
|
|
new TestCase("\"bob\\ smith\"", 2), // escape space inside of a literal |
|
}; |
|
|
|
|
|
Search("QueryString search with text", testCases, client, x => x.Name, QueryStringSearch); |
|
Search("QueryString search with text + field data", testCases, client, x => x.NameWithFieldData, QueryStringSearch); |
|
Search("QueryString search with text + field data + analyzer", testCases, client, x => x.NameWithFieldDataAndAnalyzer, QueryStringSearch); |
|
Search("QueryString search By Keyword", testCases, client, x => x.NameWithKeyword, QueryStringSearch); |
|
|
|
Search("Match search with text", testCases, client, x => x.Name, MatchSearch); |
|
Search("Match search with text + field data", testCases, client, x => x.NameWithFieldData, MatchSearch); |
|
Search("Match search with text + field data + analyzer", testCases, client, x => x.NameWithFieldDataAndAnalyzer, MatchSearch); |
|
Search("Match search By Keyword", testCases, client, x => x.NameWithKeyword, MatchSearch); |
|
|
|
//none of these work at all. commented to remove clutter |
|
//Search("Term search with text", testCases, client, x => x.Name, TermSearch); |
|
//Search("Term search with text + field data", testCases, client, x => x.NameWithFieldData, TermSearch); |
|
//Search("Term search with text + field data + analyzer", testCases, client, x => x.NameWithFieldDataAndAnalyzer, TermSearch); |
|
//Search("Term search By Keyword", testCases, client, x => x.NameWithKeyword, TermSearch); |
|
} |
|
|
|
private void Search(string name, IEnumerable<TestCase> cases, ElasticClient client, Expression<Func<Student, object>> field, |
|
Action<ElasticClient, TestCase, Expression<Func<Student, object>>> func) |
|
{ |
|
Terminal.Magenta(name); |
|
foreach (var c in cases) |
|
func(client, c, field); |
|
} |
|
|
|
private static ElasticClient GetClient() |
|
{ |
|
var elasticSettings = new ConnectionSettings(new Uri("http://localhost:9200/")); |
|
elasticSettings.DefaultIndex(index); |
|
elasticSettings.EnableDebugMode(); |
|
elasticSettings.DisableDirectStreaming(); |
|
var client = new ElasticClient(elasticSettings); |
|
return client; |
|
} |
|
|
|
private void Sanity(ElasticClient client) |
|
{ |
|
var response = client.Search<Student>(a => a |
|
.Query(q => q.MatchAll()) |
|
); |
|
|
|
var hits = response.Hits.Count; |
|
Terminal.Cyan($"Total records in index: {hits}"); |
|
} |
|
|
|
|
|
private void QueryStringSearch(ElasticClient client, TestCase testCase, Expression<Func<Student, object>> field) |
|
{ |
|
var query = Query<Student>.QueryString(qs => qs |
|
.Fields(f => f.Field(field)) |
|
.Query(testCase.Term) |
|
); |
|
var response = client.Search<Student>(a => a.Query(_ => query)); |
|
var hits = response.Hits.Count; |
|
var good = $"Query {testCase.Term,20} matches with {hits}"; |
|
var bad = $"Query {testCase.Term,20} returned {hits} instead of {testCase.ExpectedResults}"; |
|
Terminal.GoodBad(good, bad, hits == testCase.ExpectedResults); |
|
} |
|
|
|
private void MatchSearch(ElasticClient client, TestCase testCase, Expression<Func<Student, object>> field) |
|
{ |
|
var query = Query<Student>.Match(m => m.Field(field).Query(testCase.Term)); |
|
var response = client.Search<Student>(a => a.Query(_ => query)); |
|
|
|
var hits = response.Hits.Count; |
|
var good = $"Match {testCase.Term,20} matches with {hits}"; |
|
var bad = $"Match {testCase.Term,20} returned {hits} instead of {testCase.ExpectedResults}"; |
|
Terminal.GoodBad(good, bad, hits == testCase.ExpectedResults); |
|
} |
|
|
|
private void TermSearch(ElasticClient client, TestCase testCase, Expression<Func<Student, object>> field) |
|
{ |
|
var query = Query<Student>.Term(field, testCase.Term); |
|
var response = client.Search<Student>(a => a.Query(_ => query)); |
|
|
|
var hits = response.Hits.Count; |
|
var good = $"Match {testCase.Term,20} matches with {hits}"; |
|
var bad = $"Match {testCase.Term,20} returned {hits} instead of {testCase.ExpectedResults}"; |
|
Terminal.GoodBad(good, bad, hits == testCase.ExpectedResults); |
|
} |
|
|
|
private static void Seed(ElasticClient client) |
|
{ |
|
var data = new[] |
|
{ |
|
new Student |
|
{ |
|
Name = "bob smith", |
|
}, |
|
new Student |
|
{ |
|
Name = "bob jones", |
|
}, |
|
new Student |
|
{ |
|
Name = "john smith", |
|
}, |
|
}; |
|
var indexResponse = client.IndexMany(data); |
|
|
|
if (indexResponse.IsValid) |
|
Terminal.Green("Indexed data successfully"); |
|
} |
|
|
|
private static void RecreateIndex(ElasticClient client) |
|
{ |
|
var sortAnalyzer = new CustomAnalyzer |
|
{ |
|
Filter = new List<string> |
|
{ |
|
"lowercase" |
|
}, |
|
Tokenizer = "keyword" |
|
}; |
|
|
|
var delResponse = client.DeleteIndex(indexName); |
|
|
|
if (!delResponse.Acknowledged) |
|
Terminal.Yellow($"Didnt need to delete index"); |
|
else |
|
Terminal.Green($"Index {index} deleted"); |
|
|
|
var resp = client.CreateIndex(indexName, x => x |
|
.Settings(z => z |
|
.Analysis(ad => ad |
|
.Analyzers(fdab => fdab |
|
.Custom(SortAnalyzerName, _ => sortAnalyzer) |
|
)) |
|
).Mappings(m => m |
|
.Map<Student>(s => s.Properties(p => p |
|
.Text(t => t.Name(z => z.Name)) |
|
.Text(t => t.Name(z => z.NameWithFieldData).Fielddata()) |
|
.Text(t => t.Name(z => z.NameWithFieldDataAndAnalyzer).Analyzer(SortAnalyzerName).Fielddata()) |
|
.Keyword(t => t.Name(z => z.NameWithKeyword)) |
|
)) |
|
)); |
|
if (!resp.Acknowledged) |
|
{ |
|
Terminal.Red($"Failed to create index"); |
|
return; |
|
} |
|
Terminal.Green($"index {index} created"); |
|
|
|
//var mapping = client.GetMapping<Student>(); |
|
//Terminal.Blue(mapping.DebugInformation); |
|
} |
|
|
|
} |
|
|
|
public class Student |
|
{ |
|
public string Name { get; set; } |
|
public string NameWithFieldData => Name; |
|
public string NameWithFieldDataAndAnalyzer => Name; |
|
public string NameWithKeyword => Name; |
|
|
|
public List<StudentAddress> Addresses { get; set; } = new List<StudentAddress>(); |
|
} |
|
|
|
public class StudentAddress |
|
{ |
|
public string Line1 { get; set; } |
|
public string Line2 { get; set; } |
|
public string City { get; set; } |
|
public string State { get; set; } |
|
} |
|
|
|
public class TestCase |
|
{ |
|
public TestCase() { } |
|
public TestCase(string query, int expectedResults) |
|
{ |
|
Term = query; |
|
ExpectedResults = expectedResults; |
|
} |
|
|
|
public string Term { get; set; } |
|
public int ExpectedResults { get; set; } |
|
} |
|
} |