I'm trying to boost some fields over others in a multiMatch search.
Looking at the docs I see you can create a Field with boost by doing this
var titleField = Infer.Field<Page>(p => p.Title, 2);
I haven't been able to figure out how that translates to Fields though.
Something like this isn't right
var bodyField = Infer.Field<Page>(p => p.Body);
var titleField = Infer.Field<Page>(p => p.Title, 2);
var metaDescriptionField = Infer.Field<Page>(p => p.MetaDescription, 1.5);
var metaKeywordsField = Infer.Field<Page>(p => p.Keywords, 2);
MultiMatchQuery multiMatchQuery = new MultiMatchQuery()
{
Fields = Infer.Fields<Page>(bodyField, titleField, metaDescriptionField, metaKeywordsField),
Query = search.Term
};
Do I need to use the string names for the fields like
var titleFieldString = "Title^2";
and pass those into Infer.Fields
You can use the strongly typed Infer.Field<T>(); there is an implicit conversion from Field to Fields, and additional fields can be added with .And(). Here's an example
void Main()
{
var client = new ElasticClient();
Fields bodyField = Infer.Field<Page>(p => p.Body);
var titleField = Infer.Field<Page>(p => p.Title, 2);
var metaDescriptionField = Infer.Field<Page>(p => p.MetaDescription, 1.5);
var metaKeywordsField = Infer.Field<Page>(p => p.Keywords, 2);
var searchRequest = new SearchRequest<Page>()
{
Query = new MultiMatchQuery()
{
Fields = bodyField
.And(titleField)
.And(metaDescriptionField)
.And(metaKeywordsField),
Query = "multi match search term"
}
};
client.Search<Page>(searchRequest);
}
public class Page
{
public string Body { get; set; }
public string Title { get; set; }
public string MetaDescription { get; set; }
public string Keywords { get; set; }
}
this yields
{
"query": {
"multi_match": {
"query": "multi match search term",
"fields": [
"body",
"title^2",
"metaDescription^1.5",
"keywords^2"
]
}
}
}
You can also pass an array of Field which also implicitly converts to Fields
var searchRequest = new SearchRequest<Page>()
{
Query = new MultiMatchQuery()
{
Fields = new[] {
bodyField,
titleField,
metaDescriptionField,
metaKeywordsField
},
Query = "multi match search term"
}
};
As well as pass an array of strings
var searchRequest = new SearchRequest<Page>()
{
Query = new MultiMatchQuery()
{
Fields = new[] {
"body",
"title^2",
"metaDescription^1.5",
"keywords^2"
},
Query = "multi match search term"
}
};
Related
I have a query and want to do a search by multiple fields in case if no result by uuid need to do the same by parentUuid:
C# query:
return new NestedQuery
{
Path = Infer.Field<ElasticDocument>(t => t.KeywordFields),
Query = new BoolQuery
{
Filter = new List<QueryContainer>
{
new TermsQuery
{
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields),
Terms = new List<string>
{
"term2"
},
Boost = 10
}
},
Should = new List<QueryContainer>
{
new TermQuery
{
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields.First().UUID),
Value = filter.UUID
},
new TermQuery
{
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields.First().ParentUUID),
Value = filter.UUID
},
}
}
};
example of documents:
"keywordFields": [
{
"value": "term1",
"uuid": "bf18ee9f-7592-488d-7985-2b9fe8b878ca",
"parentUUID": null
},
{
"value": "term2",
"uuid": "079205ed-30df-08f6-02a1-9caf093c3be0",
"parentUUID": "103d6061-cb99-4fba-8118-2ea501e4425d"
}
]
How to update the query to use or condition in case if no result by 'UUID' then let's do it by ParentUUID?
Try to combine bool query:
nested1 = new NestedQuery {
Path = Infer.Field<ElasticDocument>(t => t.KeywordFields),
Query = new BoolQuery {
Filter = new List<QueryContainer>
{
new TermsQuery {
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields),
Terms = new List<string>
{
"term2"
},
Boost = 10
}
}
}
};
nested2 = new NestedQuery {
Path = Infer.Field<ElasticDocument>(t => t.KeywordFields),
Query = new BoolQuery {
Filter = new List<QueryContainer>
{
new TermsQuery {
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields),
Terms = new List<string>
{
"term2"
},
Boost = 10
}
},
Should = new List<QueryContainer>
{
new BoolQuery {
MustNot = new List<QueryContainer> {
new TermQuery
{
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields.First().UUID),
Value = filter.UUID
}
},
Must = new List<QueryContainer> {
new TermQuery
{
Field = Infer.Field<ElasticDocument>(t => t.KeywordFields.First().parentUUID),
Value = filter.UUID
}
}
}
}
}
};
And should nested1 with nested2
note: not IDE for this edit, the nested2 query could be simplify for sure.
I am trying to create some completion suggesters on some of my fields. My document class looks like this:
[ElasticType(Name = "rawfiles", IdProperty = "guid")]
public class RAW
{
[ElasticProperty(OmitNorms = true, Index = FieldIndexOption.NotAnalyzed, Type = FieldType.String, Store = true)]
public string guid { get; set; }
[ElasticProperty(OmitNorms = true, Index = FieldIndexOption.Analyzed, Type = FieldType.String, Store = true, IndexAnalyzer = "def_analyzer", SearchAnalyzer = "def_analyzer_search", AddSortField = true)]
public string filename { get; set; }
[ElasticProperty(OmitNorms = true, Index = FieldIndexOption.Analyzed, Type = FieldType.String, Store = true, IndexAnalyzer = "def_analyzer", SearchAnalyzer = "def_analyzer_search")]
public List<string> tags { get { return new List<string>(); } }
}
And here is how I am trying to create the completion fields
public bool CreateMapping(ElasticClient client, string indexName)
{
IIndicesResponse result = null;
try
{
result = client.Map<RAW>(
c => c.Index(indexName)
.MapFromAttributes()
.AllField(f => f.Enabled(false))
.SourceField(s => s.Enabled())
.Properties(p => p
.Completion(s => s.Name(n => n.tags.Suffix("comp"))
.IndexAnalyzer("standard")
.SearchAnalyzer("standard")
.MaxInputLength(20)
.Payloads()
.PreservePositionIncrements()
.PreserveSeparators())
.Completion(s2 => s2.Name(n=>n.filename.Suffix("comp"))
.IndexAnalyzer("standard")
.SearchAnalyzer("standard")
.MaxInputLength(20)
.Payloads()
.PreservePositionIncrements()
.PreserveSeparators())
)
);
}
catch (Exception)
{
}
return result != null && result.Acknowledged;
}
My problem is that this is only creating a single completion field named "comp". I was under the impression that this will create two completion fields, one named filename.comp and the other named tags.comp.
I then tried the answer on this SO question but this complicated the matter even worse as now my two fields were mapped as a completion field only.
Just to be clear, I want to create a multi-field (field) that has a data, sort and completion fileds. Much like the one in this example
This is how you can reproduce auto-complete example from attached by you article.
My simple class(we are going to implement auto-complete on Name property)
public class Document
{
public int Id { get; set; }
public string Name { get; set; }
}
To create multi field mapping in NEST we have to define mapping in such manner:
var indicesOperationResponse = client.CreateIndex(descriptor => descriptor
.Index(indexName)
.AddMapping<Document>(m => m
.Properties(p => p.MultiField(mf => mf
.Name(n => n.Name)
.Fields(f => f
.String(s => s.Name(n => n.Name).Index(FieldIndexOption.Analyzed))
.String(s => s.Name(n => n.Name.Suffix("sortable")).Index(FieldIndexOption.NotAnalyzed))
.String(s => s.Name(n => n.Name.Suffix("autocomplete")).IndexAnalyzer("shingle_analyzer"))))))
.Analysis(a => a
.Analyzers(b => b.Add("shingle_analyzer", new CustomAnalyzer
{
Tokenizer = "standard",
Filter = new List<string> {"lowercase", "shingle_filter"}
}))
.TokenFilters(b => b.Add("shingle_filter", new ShingleTokenFilter
{
MinShingleSize = 2,
MaxShingleSize = 5
}))));
Let's index some documents:
client.Index(new Document {Id = 1, Name = "Tremors"});
client.Index(new Document { Id = 2, Name = "Tremors 2: Aftershocks" });
client.Index(new Document { Id = 3, Name = "Tremors 3: Back to Perfection" });
client.Index(new Document { Id = 4, Name = "Tremors 4: The Legend Begins" });
client.Index(new Document { Id = 5, Name = "True Blood" });
client.Index(new Document { Id = 6, Name = "Tron" });
client.Index(new Document { Id = 7, Name = "True Grit" });
client.Index(new Document { Id = 8, Name = "Land Before Time" });
client.Index(new Document { Id = 9, Name = "The Shining" });
client.Index(new Document { Id = 10, Name = "Good Burger" });
client.Refresh();
Now, we are ready to write prefix query :)
var searchResponse = client.Search<Document>(s => s
.Query(q => q
.Prefix("name.autocomplete", "tr"))
.SortAscending(sort => sort.Name.Suffix("sortable")));
This query will get us
Tremors 2: Aftershocks
Tremors 3: Back to Perfection
Tremors 4: The Legend Begins
Tron
True Blood
True Grit
Hope this will help you.
Recently, guys from NEST prepared great tutorial about NEST and elasticsearch. There is a part about suggestions, it should be really useful for you.
I am writing a query in nest for elasticsearch that matches to a list of countries - it cutrrently matches whenever any of the countries in the list is present in ESCountryDescription (a list of countries). I only want to match when all of the countries in CountryList match ESCountryDescription. I believe that I need to use MinimumShouldMatch as in this example http://www.elastic.co/guide/en/elasticsearch/reference/0.90/query-dsl-terms-query.html
a.Terms(t => t.ESCountryDescription, CountryList)
But I cannot find a way of adding MinimumShouldMatch into my query above.
You can apply MinimumShouldMatch patameter in TermsDescriptor. Here is an example:
var lookingFor = new List<string> { "netherlands", "poland" };
var searchResponse = client.Search<IndexElement>(s => s
.Query(q => q
.TermsDescriptor(t => t.OnField(f => f.Countries).MinimumShouldMatch("100%").Terms(lookingFor))));
or
var lookingFor = new List<string> { "netherlands", "poland" };
var searchResponse = client.Search<IndexElement>(s => s
.Query(q => q
.TermsDescriptor(t => t.OnField(f => f.Countries).MinimumShouldMatch(lookingFor.Count).Terms(lookingFor))));
And this is the whole example
class Program
{
public class IndexElement
{
public int Id { get; set; }
[ElasticProperty(Index = FieldIndexOption.NotAnalyzed)]
public List<string> Countries { get; set; }
}
static void Main(string[] args)
{
var indexName = "sampleindex";
var uri = new Uri("http://localhost:9200");
var settings = new ConnectionSettings(uri).SetDefaultIndex(indexName).EnableTrace(true);
var client = new ElasticClient(settings);
client.DeleteIndex(indexName);
client.CreateIndex(
descriptor =>
descriptor.Index(indexName)
.AddMapping<IndexElement>(
m => m.MapFromAttributes()));
client.Index(new IndexElement {Id = 1, Countries = new List<string> {"poland", "germany", "france"}});
client.Index(new IndexElement {Id = 2, Countries = new List<string> {"poland", "france"}});
client.Index(new IndexElement {Id = 3, Countries = new List<string> {"netherlands"}});
client.Refresh();
var lookingFor = new List<string> { "germany" };
var searchResponse = client.Search<IndexElement>(s => s
.Query(q => q
.TermsDescriptor(t => t.OnField(f => f.Countries).MinimumShouldMatch("100%").Terms(lookingFor))));
}
}
Regarding your problem
For terms: "netherlands" you will get document with Id 3
For terms: "poland" and "france" you will get documents with Id 1 and 2
For terms: "germany" you will get document with Id 1
For terms: "poland", "france" and "germany" you will get document
with Id 1
I hope this is your point.
Instead of doing
.Query(q => q
.Terms(t => t.ESCountryDescription, CountryList))
You can use the command below
.Query(q => q
.TermsDescriptor(td => td
.OnField(t => t.ESCountryDescription)
.MinimumShouldMatch(x)
.Terms(CountryList)))
See this for unit tests in elasticsearch-net Github repository.
I have a model,
public class Customer
{
public string Name { get; set;}
public string CountryCode { get; set;}
}
In the controller
var model = new List<Customer>
{
new Customer { Name = "foo", CountryCode = "US"},
new Customer { Name = "bar", CountryCode = "UK",
};
return PartialView("_Edit", model);
An extension method for displaying all countries:-
public class CountryList
{
public static IEnumerable<SelectListItem> CountrySelectList
{
get
{
var list = new List<SelectListItem>()
{
new SelectListItem { Value = "US", Text="US" },
new SelectListItem { Value = "UK", Text="UK" },
};
return list;
}
}
}
In the PartialView
#model List<Customer>
#Html.DropDownListFor(model => model[i].CountryCode, CountryList.CountrySelectList, "Select Country Type")
But the drop down doesn't select each customer's country code? Any thoughts?
PS: It is using model[i] => which is of type Customer, for simplicity i had removed the forloop before rendering the html tags.
#using(Html.BeginForm())
{
for(int i = 0; i < Model.Count(); i++)
{
#Html.TextBoxFor(model => model[i].Name)
#Html.DropDownListFor..........
}
}
Because your CoutryList helper does returns a list of SelectListItems that all have Selected property set to False (which is default).
I would rewrite your helper method as follows:
public static IEnumerable<SelectListItem> CountrySelectList(string selectedCountryCode)
{
get
{
var list = new List<SelectListItem>()
{
new SelectListItem { Value = "US", Text="US" },
new SelectListItem { Value = "UK", Text="UK" },
};
var selectedListItem = list.FirstOrDefault(t=>t.Value== selectedCountryCode);
if(selectedListItem!=null)
selectedListItem.Selected=true;
return list;
}
}
In view:
#Html.DropDownListFor(model => model[i].Customer, CountryList.CountrySelectList(model[i].Customer.CountryCode), "Select Country Type")
I have a two objects as follows:
public class Item
{
public int ItemId {get;set;}
public string ItemName {get;set;}
public List<Tag> ItemTags {get;set;}
public DateTime DateCreated {get;set;}
}
public class Tag
{
public int TagId {get;set;}
public string TagName {get;set;}
}
These are LINQ-to-SQL objects, so the ItemTags will be an EntitySet.
I am trying to perform a search query where a user can provide a comma delimited list of tags as a search filter.
How do I filter my list of items to those which contains all of the tags in the comma delimited list.
EDIT2
e.g.
Item1 has tags of Apple, Banana, Orange
Item2 has tags of Banana, Orange
Item3 has tags of Pineapple, Orange
If the tag filter is "Banana, Orange" I need the results to be Item1 and Item2.
/EDIT2
This is what I have tried thus far:
string tags = "Manchester United,European Cup,2008";
List<string> tagsList = tags.Trim().ToLower()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.ToList();
List<Item> itemList = ItemRepository.FetchAll();
var query = itemList
.OrderByDescending(p => p.DateCreated)
.ToList();
if (tagsList.Count() > 0)
{
query = query
.Where(p => p.ItemTags
.Select(q => q.TagName.ToLower())
.All(r => tagsList.Contains(r)))
.ToList();
}
However, this doesn't seem to work. Any ideas on what I am doing wrong please?
EDIT1: tags are trimmed and are 'lowercased'.
That because you're puting the tags from the items to lowercase, but not the searched tags.
With this modification it should work:
List<string> tagsList = tags
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.ToLower())
.Distinct()
.ToList();
EDIT: OK, I see what the problem is: you're doing it backwards. You're searching for items that have only the tags that you're looking for.
Try that instead:
query =
(from item in query
let itemTags = p.ItemTags.Select(it => it.TagName.ToLower())
where tags.All(t => itemTags.Contains(t))
select item).ToList();
UPDATE: here's a version with the lambda syntax. It's pretty ugly because of the temporary anonymous type, but that's how the let clause translates to lambda...
query =
query.Select(item => new { item, itemTags = item.ItemTags.Select(it => it.TagName.ToLower()) })
.Where(x => tagsList.All(t => x.itemTags.Contains(t)))
.Select(x => x.item)
.ToList();
I think you need to do something like this:
var query = itemList.OrderByDescending(p => p.DateCreated).ToList();
var results = query.Where(i => i.ItemTags
.All(it => tagsList.Contains(it.TagName.ToLower())));
Then results should then be a list of matching items.
PS. Your code shows you fetching itemList as a List from your repository and then sorting by date created. This means the sorting isn't being done in the database. Once you turn something into a List you give up the benefits of deferred execution as you will bring back the entire collection into memory.
EDIT: Here's the test code to prove it works in Linq to Objects:
public class Item
{
public int ItemId { get; set; }
public string ItemName { get; set; }
public List<Tag> ItemTags { get; set; }
public DateTime DateCreated { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public string TagName { get; set; }
}
class Program
{
static void Main(string[] args)
{
RunTags();
}
private static void RunTags()
{
Item i1 = new Item()
{
ItemId = 1,
ItemName = "Item1",
ItemTags = new List<Tag>() { new Tag { TagId = 1, TagName = "2008" }, new Tag { TagId = 2, TagName = "Donkey" } }
};
Item i2 = new Item()
{
ItemId = 2,
ItemName = "Item2",
ItemTags = new List<Tag>() { new Tag { TagId = 4, TagName = "Cat" }, new Tag { TagId = 2, TagName = "Donkey" }, new Tag { TagId = 3, TagName = "Seattle" } }
};
Item i3 = new Item()
{
ItemId = 3,
ItemName = "Item3",
ItemTags = new List<Tag>() { new Tag { TagId = 523, TagName = "Manchester united" }, new Tag { TagId = 10, TagName = "European Cup" }, new Tag { TagId = 1, TagName = "2008" } }
};
Item i4 = new Item()
{
ItemId = 4,
ItemName = "Item4",
ItemTags = new List<Tag>() { new Tag { TagId = 05, TagName = "Banana" }, new Tag { TagId = 140, TagName = "Foo" }, new Tag { TagId = 4, TagName = "Cat" } }
};
Item i5 = new Item()
{
ItemId = 5,
ItemName = "Item5",
ItemTags = new List<Tag>() { new Tag { TagId = 05, TagName = "Banana" }, new Tag { TagId = 140, TagName = "Foo" } }
};
List<Item> itemList = new List<Item>() { i1, i2, i3, i4, i5 };
string tags = "Manchester United,European Cup,2008";
List<string> tagsList = tags.Trim().ToLower()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.ToList();
var query = itemList
.OrderByDescending(p => p.DateCreated).ToList();
var results = query.Where(i => i.ItemTags.All(it => tagsList.Contains(it.TagName.ToLower())));
foreach (var item in results)
{
Console.WriteLine(item.ItemName); // Should return "Item3"
}
Console.ReadLine();
}
If you want to match any of the tags in the Item's ItemTag list then just change All to Any i.e.
var results = query.Where(i => i.ItemTags.Any(it => tagsList.Contains(it.TagName.ToLower())));