diff --git a/Elasticsearch/Search.Elasticsearch/Indexing/IndexBaseItem.cs b/Elasticsearch/Search.Elasticsearch/Indexing/IndexBaseItem.cs index 49fc1d0..838d103 100644 --- a/Elasticsearch/Search.Elasticsearch/Indexing/IndexBaseItem.cs +++ b/Elasticsearch/Search.Elasticsearch/Indexing/IndexBaseItem.cs @@ -50,11 +50,15 @@ protected static IAnalysis InitCommonAnalyzers(AnalysisDescriptor analysis) .Custom("autocomplete", ca => ca .Tokenizer("autocomplete") .Filters("stopwords_eng", "trim", "lowercase") - ) + ) .Custom("autocomplete_search", ca => ca .Tokenizer("standard") .Filters("stopwords_eng", "trim", "lowercase") - ) + ) + .Custom("keyword_list_serach", ca => ca + .Tokenizer("split_list") + .Filters("stopwords_eng", "trim") + ) ) .Tokenizers(tdesc => tdesc .EdgeNGram("autocomplete", e => e @@ -62,7 +66,8 @@ protected static IAnalysis InitCommonAnalyzers(AnalysisDescriptor analysis) .MaxGram(15) .TokenChars(TokenChar.Letter, TokenChar.Digit) ) - ) + .Pattern("split_list", e => e.Pattern(",")) + ) .TokenFilters(f => f .Stop("stopwords_eng", lang => lang .StopWords("_english_") diff --git a/Elasticsearch/Search.Elasticsearch/Indexing/IndexManagementItem.cs b/Elasticsearch/Search.Elasticsearch/Indexing/IndexManagementItem.cs index 42bcb7f..02bbf9e 100644 --- a/Elasticsearch/Search.Elasticsearch/Indexing/IndexManagementItem.cs +++ b/Elasticsearch/Search.Elasticsearch/Indexing/IndexManagementItem.cs @@ -12,13 +12,30 @@ public class IndexManagementItem : public override async Task CreateIndex(IElasticClient aClient) { - await this.DeleteIndex(aClient); + await this.DeleteIndex(aClient); var res = await aClient.Indices.CreateAsync(Name, i => i - .Settings(InitCommonIndexSettingsDescriptor) - .Map(m => m.AutoMap()) - ); - + .Settings(InitCommonIndexSettingsDescriptor) + .Map(m => m + .AutoMap() + .Properties(ps => ps + .Text(p => p + .Name("Market") + .Fields(fs => fs + .Text(f => f + .Name("phrase") + .Analyzer("english") + ) + .Keyword(f => f + .Name("keyword") + ) + ) + .Analyzer("autocomplete") + .SearchAnalyzer("autocomplete_search") + ) + ) + ) + ); return res; } diff --git a/Elasticsearch/Search.Elasticsearch/Indexing/IndexPropertyItem.cs b/Elasticsearch/Search.Elasticsearch/Indexing/IndexPropertyItem.cs index 809337f..cfa8e28 100644 --- a/Elasticsearch/Search.Elasticsearch/Indexing/IndexPropertyItem.cs +++ b/Elasticsearch/Search.Elasticsearch/Indexing/IndexPropertyItem.cs @@ -16,7 +16,36 @@ public override async Task CreateIndex(IElasticClient aClie var res = await aClient.Indices.CreateAsync(Name, i => i .Settings(InitCommonIndexSettingsDescriptor) - .Map(m => m.AutoMap()) + .Map(m => m + .AutoMap() + .Properties( ps => ps + .Text(p => p + .Name("Market") + .Fields(fs => fs + .Text(f => f + .Name("phrase") + .Analyzer("english") + ) + .Keyword(f => f + .Name("keyword") + ) + ) + .Analyzer("autocomplete") + .SearchAnalyzer("autocomplete_search") + ) + .Text( p => p + .Name("City") + .Fields( fs => fs + .Text(f => f + .Name("phrase") + .Analyzer("english") + ) + ) + .Analyzer("autocomplete") + .SearchAnalyzer("autocomplete_search") + ) + ) + ) ); return res; diff --git a/Elasticsearch/Search.Elasticsearch/Mapping/SearchableBaseItem.cs b/Elasticsearch/Search.Elasticsearch/Mapping/SearchableBaseItem.cs index 0d36e53..99f23ff 100644 --- a/Elasticsearch/Search.Elasticsearch/Mapping/SearchableBaseItem.cs +++ b/Elasticsearch/Search.Elasticsearch/Mapping/SearchableBaseItem.cs @@ -7,14 +7,22 @@ public class SearchableBaseItem [Keyword(Name = nameof(Id))] public int Id { get; set; } + [Keyword(Name = nameof(TypeName))] + public string TypeName { get; protected set; } + [Text(Analyzer = "autocomplete", SearchAnalyzer = "autocomplete_search", Name = nameof(Name))] public string Name { get; set; } - [Text(Analyzer = "autocomplete", SearchAnalyzer = "autocomplete_search", Name = nameof(Market))] + [Keyword(Name = nameof(State))] + public string State { get; set; } + + //multifieds : mapping in code + [Text(Name = nameof(Market))] public string Market { get; set; } + + [Ignore] + public virtual string AdditionalInfo { get; } - public string State { get; set; } - public SearchableBaseItem() { } diff --git a/Elasticsearch/Search.Elasticsearch/Mapping/SearchableManagementItem.cs b/Elasticsearch/Search.Elasticsearch/Mapping/SearchableManagementItem.cs index f4fc056..f6d24f1 100644 --- a/Elasticsearch/Search.Elasticsearch/Mapping/SearchableManagementItem.cs +++ b/Elasticsearch/Search.Elasticsearch/Mapping/SearchableManagementItem.cs @@ -2,9 +2,14 @@ namespace Search.Elasticsearch.Mapping { - [ElasticsearchType(IdProperty = nameof(SearchableBaseItem.Id), RelationName = SearchableManagementItem.TypeName)] + [ElasticsearchType(IdProperty = nameof(SearchableBaseItem.Id), RelationName = SearchableManagementItem.TypeNameDef)] public class SearchableManagementItem : SearchableBaseItem { - public const string TypeName = "searchablemanagementitem"; + public const string TypeNameDef = "searchablemanagementitem"; + + public SearchableManagementItem() + { + this.TypeName = TypeNameDef; + } } } diff --git a/Elasticsearch/Search.Elasticsearch/Mapping/SearchablePropertyItem.cs b/Elasticsearch/Search.Elasticsearch/Mapping/SearchablePropertyItem.cs index 6f121d8..2fdd8b2 100644 --- a/Elasticsearch/Search.Elasticsearch/Mapping/SearchablePropertyItem.cs +++ b/Elasticsearch/Search.Elasticsearch/Mapping/SearchablePropertyItem.cs @@ -2,10 +2,10 @@ namespace Search.Elasticsearch.Mapping { - [ElasticsearchType(IdProperty = nameof(SearchableBaseItem.Id), RelationName = SearchablePropertyItem.TypeName)] + [ElasticsearchType(IdProperty = nameof(SearchableBaseItem.Id), RelationName = SearchablePropertyItem.TypeNameDef)] public class SearchablePropertyItem : SearchableBaseItem { - public const string TypeName = "searchablepropertyitem"; + public const string TypeNameDef = "searchablepropertyitem"; [Text(Analyzer = "autocomplete", SearchAnalyzer = "autocomplete_search", Name = nameof(FormerName))] public string FormerName { get; set; } @@ -13,11 +13,20 @@ public class SearchablePropertyItem : SearchableBaseItem [Text(Analyzer = "autocomplete", SearchAnalyzer = "autocomplete_search", Name = nameof(StreetAddres))] public string StreetAddres { get; set; } - [Text(Analyzer = "autocomplete", SearchAnalyzer = "autocomplete_search", Name = nameof(City))] + //multifieds : mapping in code + [Text(Name = nameof(City))] public string City { get; set; } public float Lat { get; set; } public float Lng { get; set; } + + public override string AdditionalInfo { get { return $"FormerName:{this.FormerName} City:{this.City} Street:{this.StreetAddres}"; } } + + public SearchablePropertyItem() + { + this.TypeName = TypeNameDef; + } + } } diff --git a/Elasticsearch/Search.Elasticsearch/Search.Elasticsearch.csproj b/Elasticsearch/Search.Elasticsearch/Search.Elasticsearch.csproj index c7f58b0..53cac79 100644 --- a/Elasticsearch/Search.Elasticsearch/Search.Elasticsearch.csproj +++ b/Elasticsearch/Search.Elasticsearch/Search.Elasticsearch.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -6,6 +6,7 @@ + diff --git a/Elasticsearch/Search.Elasticsearch/Search/SearchService.cs b/Elasticsearch/Search.Elasticsearch/Search/SearchService.cs index 81c08e0..6145017 100644 --- a/Elasticsearch/Search.Elasticsearch/Search/SearchService.cs +++ b/Elasticsearch/Search.Elasticsearch/Search/SearchService.cs @@ -1,4 +1,5 @@ using Nest; +using RestSharp; using Search.Elasticsearch.Mapping; using System.Collections.Generic; using System.Threading.Tasks; @@ -25,45 +26,114 @@ public async Task Search(SimpleSearchRequest aSearchReques BoolQuery filterQuery = new BoolQuery(); if (!string.IsNullOrEmpty(aSearchRequest.MarketFilterQuery)) { + //we assume that: + // 1: the MarketFilterQuery is in fromat Market1,Market2,Market3 e.g. Austin, San Paulo + // 2: the market names are coming from predefined table, so no spelling mistakes, no autocompletition + // 3: keyword field, and keyword_list_serach analyzer are created with the index var filterQueryParts = new List(); filterQueryParts.Add( new MatchQuery() { - Field = $"{nameof(SearchableBaseItem.Market)}", - Query = aSearchRequest.MarketFilterQuery.ToLower(), - Fuzziness = Fuzziness.Auto - } + Field = "Market.keyword", + Query = aSearchRequest.MarketFilterQuery,//.ToLower(), + Analyzer = "keyword_list_serach" + } ); filterQuery.Filter = filterQueryParts; } - var results = await _client.SearchAsync(s => s + //check the API to be able to get the specific SearchableBaseItem derived types (something similar to ConcreteTypeSelector?) + //for now we use JsonObject and convert it to specific object base on the "TypeName" + + var results = await _client.SearchAsync(s => s .Size(aSearchRequest.PageSize) .Skip(aSearchRequest.PageStartIndex) .Index(Indices.Index(aSearchRequest.Indices)) - .Query(q => q - .MultiMatch(m => m - .Query(aSearchRequest.AllStringFiledsQuery.ToLower()) - .Fields(ff => ff - .Field($"{nameof(SearchableBaseItem.Name)}") - .Field($"{nameof(SearchableBaseItem.Market)}") - .Field($"{nameof(SearchableBaseItem.State)}") - .Field($"{nameof(SearchablePropertyItem.FormerName)}") - .Field($"{nameof(SearchablePropertyItem.StreetAddres)}") - .Field($"{nameof(SearchablePropertyItem.City)}") - ) - //.Fuzziness(Fuzziness.Auto) - ) - && filterQuery - ) - ); + .Query(q => q + .Bool(b => b + .Should( + // for City and Market a 'phrase' field is crated, which allowes + // to better place the item in result when the city/market was put in the AllStringFiledsQuery + bs => bs.MatchPhrase(x => x + .Query(aSearchRequest.AllStringFiledsQuery.ToLower()) + .Field("City.phrase") + ), + bs => bs.MatchPhrase(x => x + .Query(aSearchRequest.AllStringFiledsQuery.ToLower()) + .Field("Market.phrase") + ) + ) + .Must( + // we search all text fields with the AllStringFiledsQuery + // Name, Market, FormerName, StreetAdress, City text field uses: + // 'autocomplete' analyzer when indexing data + // 'autocomplete_search' analyzer when searching + // both analyzers are crated on index creation + bs => bs.MultiMatch( m => m + .Query(aSearchRequest.AllStringFiledsQuery.ToLower()) + .Fields(ff => ff + .Field($"{nameof(SearchableBaseItem.Name)}") + .Field($"{nameof(SearchableBaseItem.Market)}") + .Field($"{nameof(SearchableBaseItem.State)}") + .Field($"{nameof(SearchablePropertyItem.FormerName)}") + .Field($"{nameof(SearchablePropertyItem.StreetAddres)}") + .Field($"{nameof(SearchablePropertyItem.City)}") + ) + .Fuzziness(Fuzziness.Auto) + ), + bs => filterQuery + ) + ) + ) + ); + + return new SimpleSerachResponse() - { + { TotalItems = results.Total, - Items = results.Documents + Items = ConvertResults(results) }; - } + } + + + private List ConvertResults(ISearchResponse aSerachResponse) + { + + List ret = new List(); + + foreach (var item in aSerachResponse.Documents) + { + switch (item["TypeName"]) + { + case "searchablemanagementitem": + ret.Add(new SearchableManagementItem() + { + Id = int.Parse(item["Id"].ToString()), + Name = (string)item["Name"], + State = (string)item["State"], + Market = (string)item["Market"] + }); + break; + case "searchablepropertyitem": + ret.Add(new SearchablePropertyItem() + { + Id = int.Parse(item["Id"].ToString()), + Name = item["Name"].ToString(), + State = item["State"].ToString(), + Market = item["Market"].ToString(), + FormerName = item.ContainsKey("FormerName") ? item["FormerName"].ToString() : "", + StreetAddres = item["StreetAddres"].ToString(), + City = (string)item["City"].ToString(), + //Lat = (float) item["lat"], + //Lng = (float) item["lng"] + }); + break; + } + } + + return ret; + } } } diff --git a/Elasticsearch/Search.IndexData.Exe/DataProvider.cs b/Elasticsearch/Search.IndexData.Exe/DataProvider.cs index 69ede2d..b67a359 100644 --- a/Elasticsearch/Search.IndexData.Exe/DataProvider.cs +++ b/Elasticsearch/Search.IndexData.Exe/DataProvider.cs @@ -23,16 +23,16 @@ public DataProvider() Market="Austin", State ="GS", StreetAddres = "3549 Curry Lane", - City = "Marietta AAA" + City = "San Francesco" }, new SearchablePropertyItem() { Id = 2, - Name ="Forest at Columbia AAAA", - StreetAddres = "1000 Merrick Ferry Road", + Name ="Forest at Columbia AAAA San in Paulo", + StreetAddres = "1000 Merrick Ferry Road San in Paulo", Market="Austin", State ="GS", - City = "Marietta ZZZ" + City = "San in Paulo" }, new SearchablePropertyItem() { @@ -41,7 +41,7 @@ public DataProvider() StreetAddres = "nowa", Market="1000 Bells Ferry Road", State ="TX", - City = "Marietta" + City = "San Paulo" }, }; @@ -51,7 +51,7 @@ public DataProvider() { Id = 1, Name = "Holland Residential", - Market="San Paulo", + Market="San Paulo AAA", State ="TX" } }; diff --git a/Elasticsearch/Search.IndexData.Exe/Program.cs b/Elasticsearch/Search.IndexData.Exe/Program.cs index 059540d..0439514 100644 --- a/Elasticsearch/Search.IndexData.Exe/Program.cs +++ b/Elasticsearch/Search.IndexData.Exe/Program.cs @@ -27,7 +27,7 @@ static void Main(string[] args) //{ // Console.WriteLine(ex); //} - + //try //{ // dataProvider.LoadProperties(@"D:\sandbox-private\GitHub\webapi\Elasticsearch\properties.json"); diff --git a/Elasticsearch/Search.WebAPI.Exe/Controllers/SearchController.cs b/Elasticsearch/Search.WebAPI.Exe/Controllers/SearchController.cs index 6fbf3f8..288110c 100644 --- a/Elasticsearch/Search.WebAPI.Exe/Controllers/SearchController.cs +++ b/Elasticsearch/Search.WebAPI.Exe/Controllers/SearchController.cs @@ -29,7 +29,7 @@ public async Task Search(SearchCriteriaDto aRequest) { _logger?.LogDebug("'{0}' has been invoked", nameof(SearchController)); - var response = new PagedResponse(); + var response = new PagedResponse(); try { var result = await _searchSevice.Search(new SimpleSearchRequest() @@ -40,7 +40,7 @@ public async Task Search(SearchCriteriaDto aRequest) MarketFilterQuery = aRequest.Market, Indices = new List() { Config.IndexPropertyItemName, Config.IndexManagementItemName } } - ); + ); response.Model = result.Items; response.ItemsCount = result.TotalItems;