How can i construct a NEST query with optional parameters? - elasticsearch

I'm using the NEST .NET client (6.3.1), and trying to compose a search query that is based on a number of (optional) parameters.
Here's what i've got so far:
var searchResponse = await _client.SearchAsync<Listing>(s => s
.Query(qq =>
{
var filters = new List<QueryContainer>();
if (filter.CategoryType.HasValue)
{
filters.Add(qq.Term(p => p.CategoryType, filter.CategoryType.Value));
}
if (filter.StatusType.HasValue)
{
filters.Add(qq.Term(p => p.StatusType, filter.StatusType.Value));
}
if (!string.IsNullOrWhiteSpace(filter.Suburb))
{
filters.Add(qq.Term(p => p.Suburb, filter.Suburb));
}
return ?????; // what do i do her?
})
);
filter is an object with a bunch of nullable properties. So, whatever has a value i want to add as a match query.
So, to achieve that i'm trying to build up a list of QueryContainer's (not sure that's the right way), but struggling to figure out how to return that as a list of AND predicates.
Any ideas?
Thanks

Ended up doing it by using the object initialisez method, instead of the Fluent DSL"
var searchRequest = new SearchRequest<Listing>
{
Query = queries
}
queries is a List<QueryContainer>, which i just build up, like this:
queries.Add(new MatchQuery
{
Field = "CategoryType",
Query = filter.CategoryType
}
I feel like there's a better way, and i don't like how i have to hardcode the 'Field' to a string... but it works. Hopefully someone shows me a better way!

Related

Dropdowns driven by ElasticSearch aggregations

Currently have dropdowns used in a search component that are driven by ES using terms aggregations. A dropdown when selected should filter the other dropdowns but not itself any further. I am currently essentially doing two searches to accomplish this and collecting aggregations from both. first is size 0 with the given query excluding the most recently applied filters:
(simplified for the post)
// for aggregation on last filter selected
var newestFilterResponse = await _client.SearchAsync<Index>(s => s
.Index(_index)
.From(0)
.Size(0)
.Query(QueryClosure(phrase, fields, filtersMinusLast))
.Aggregations(FilterOptionsAggregationClosure(lastFilter)));
// actual query for results and aggregation on other filters
var response = await _client.SearchAsync<Index>(s => s
.Index(_index)
.From(0)
.Size(500)
.Query(QueryClosure(phrase, fields, allFilters))
.Aggregations(FilterOptionsAggregationClosure(allFiltersExceptLast)));
use below to build terms aggregations for each dropdown
Func<AggregationContainerDescriptor<Index>, IAggregationContainer> FilterOptionsAggregationClosure(List<FilterButton> filterButtons)
{
return delegate (AggregationContainerDescriptor<Index> aggregationContainerDescriptor)
{
foreach (var filterButton in filterButtons)
{
aggregationContainerDescriptor = aggregationContainerDescriptor
.Terms(filterButton.AggregationName, t => t
.Field(filterButton.FieldToSearch)
.Size(10000)
);
}
return aggregationContainerDescriptor;
};
}
use below to build query (simplified to just filter for example, but there is multimatch, prefix and some more in my actual use case)
Func<QueryContainerDescriptor<Index>, QueryContainer> QueryClosure(string phrase, Fields fields, List<FilterApplied> filtersApplied)
{
return delegate (QueryContainerDescriptor<Index> queryContainerDescriptor)
{
// multimatch, prefix, etc. on phrase for fields supplied
QueryContainer queryContainer = GetKeywordQuery(phrase, fields);
QueryContainer filterQuery = new();
foreach (var filter in filtersApplied)
{
QueryContainer sameFilterQuery = new QueryContainerDescriptor<Index>()
.Bool(boolQuery => boolQuery
.Filter(f => f
.Terms(terms => terms
.Field(filter.FieldToSearch)
.Terms(filter.Values))));
filterQuery = filterQuery && sameFilterQuery;
}
return queryContainer && filterQuery;
};
}
i feel like this is not the way. Any suggestions to improve on this would be greatly appreciated. thanks!
Ended up caching dropdown options in session storage, we wanted to have url params be able to generate exact state of page when sharing links but ended up ditching that with regards to filter dropdown state so we could eliminate the need to do the isolated aggregation on the last filter type applied

projection wont work in mongodb c# driver

I have class that executes mongo queries
its works but when I send projection in query, projection won't work
and mongo return hole document
whats the matter?
query = new QueryDocument( BsonSerializer.Deserialize<BsonDocument>(queryStr));
queryStr="{family:'james'},{}" => its ok
queryStr="{},{family:0}" => not ok. return all columns, but don't want to get family column
Remember: just want this method. not another methods. because want send any query to mongo. I've read mapped mongo objects like ORMs.
just want this method. thanks
Did you look into the new C# 2.0 driver documentation? Looks like projection is a part of the options argument you can give FindAsync
private static void Find(IMongoCollection<Person> mongoCollection)
{
var query = Builders<Person>.Filter.Eq(p => p.Name, "bob");
var options = new FindOptions<Person>()
{
Projection = Builders<Person>.Projection
.Include(p => p.Name)
.Exclude(p => p.Id)
};
var result = await mongoCollection.FindAsync(query, options);
...
The BsonDocument object created from JSON {.A.}{.B.} in the question has 2 braces-pairs, and only the first one will matter (A). This is OK, since projection and query are 2 separate items.
Personally

Custom IComparer in LINQ OrderBy Lambda expression

I have a custom comparer I want to use with OrderBy. I am trying to build a LINQ expression to make it work. So in essence, I am trying to put together an IComparer, OrderBy inLinq expression.
The expression I am trying to build should look something like:
source => source.OrderBy(lambdaParameter => lambdaParameter.Name, new Parsers.NumericComparer()).
With the code below the expression
'{source => source.OrderBy(lambdaParameter => lambdaParameter.Name)}'
is built and I am trying to add this custom Icomparable to this expression
new Parsers.NumericComparer().
This is because I need to do a natural sort. Can someone please help me on how to include this expression. I am trying to read several threads for the past few hours but I have not done understood LINQ expressions well enough yet to implement this. Thanks!
private void CreateOrderByMethod(PropertyDescriptor prop, string orderByMethodName, string cacheKey)
{
/*
Create a generic method implementation for IEnumerable<T>.
Cache it.
*/
var sourceParameter = Expression.Parameter(typeof(List<T>), "source");
var lambdaParameter = Expression.Parameter(typeof(T), "lambdaParameter");
var accesedMember = typeof(T).GetProperty(prop.Name);
var propertySelectorLambda =
Expression.Lambda(Expression.MakeMemberAccess(lambdaParameter, accesedMember), lambdaParameter);
var orderByMethod = typeof(Enumerable).GetMethods()
.Where(a => a.Name == orderByMethodName &&
a.GetParameters().Length == 2)
.Single()
.MakeGenericMethod(typeof(T), prop.PropertyType);
var orderByExpression = Expression.Lambda<Func<List<T>, IEnumerable<T>>>(
Expression.Call(orderByMethod,
new Expression[] { sourceParameter,
propertySelectorLambda }),
sourceParameter);
cachedOrderByExpressions.Add(cacheKey, orderByExpression.Compile());
}
To create an expression that creates a new instance of an object, use Expression.New.
var newParser = Expression.New(typeof(Parsers.NumericComparer));
Then I suggest you use this overload of Expression.Call instead, so that you don't have to go and manually grab the MethodInfo:
var orderByCall = Expression.Call(
typeof(Enumerable),
"OrderBy",
new [] { typeof(T), prop.PropertyType },
sourceParameter, propertySelectorLambda, newParser);

LINQ ForEach with Replace

I am trying to replace a string date value "01/01/1700" with an empty string in LINQ.
The date is of type string.
Something like this but I cant get it to work.
Query<Client>(sql).ToList().ForEach(x => x.DateOfBirth =
x.DateOfBirth.Replace("01/01/1700", ""));
This code works but its not LINQ.
var result = Query<Client>(sql).ToList();
foreach (var client in result)
{
if (client.DateOfBirth == "01/01/1700")
{
client.DateOfBirth = "n/a";
}
}
Thanks for your help.
The problem is the ToList(). The result is not visible in the variable you use afterwards.
Try out the following:
var list = Query<Client>(sql).ToList();
list.ForEach(l => l.DateOfBirth = l.DateOfBirth.Replace("01/01/1700", "n/a"));
Should work fine. Use the list variable afterwards.
var result = Query<Client>(sql).ToList();
result.ForEach(l => l.DateOfBirth = l.DateOfBirth.Replace("01/01/1700", "n/a"));
Your code assumes that changes made to an object in a List will be reflected in the Query<Client> that the object came from. Apparently this is not the case. One thing you could try is assigning the list before calling ForEach() and using the list from that point on:
var clients = Query<Client>(sql).ToList();
clients.ForEach(x => x.DateOfBirth = x.DateOfBirth.Replace("01/01/1700", ""));
Also, ForEach is not a LINQ operator. It is a method in the List class. Unlike LINQ operators, it will modify the list that called it and will not return anything. The way to "modify" data with LINQ is by using select:
var clients = (from client in Query<Client>(sql).ToList()
select new Client(client)
{
DateOfBirth = client.DateOfBirth.Replace("01/01/1700", "")
}).ToList();

Linq to Entities exception

Hi I have a query like this:
var queryGridData = from question in questions
select new {
i = question.Id,
cell = new List<string>() { question.Id.ToString(), question.Note, question.Topic }
};
The ToString() part needed to convert the int is causing:
LINQ to Entities does not recognize the method 'System.String.ToString()' method, and this method cannot be translated into a store expression.
Hmmmmmmmmmmm. I need it as a string to go into the collection. Any ideas?
I would personally perform just enough of the query in the database to provide the values you want, and do the rest in .NET:
var queryGridData = questions.Select(q => new { q.Id, q.Note, q.Topic })
.AsEnumerable() // Do the rest locally
.Select(q => new { i = q.Id,
cell = new List<string> {
q.Id.ToString(),
q.Note,
q.Topic
} });
(This formatting is horrible, but hopefully it'll be easier to do nicely in an IDE where you've got more space :)

Resources