Nest - how to write a span near query with multiple clauses? - elasticsearch

I want to use NEST to write a query as described in:
https://www.elastic.co/guide/en/elasticsearch/reference/1.7/query-dsl-span-near-query.html
I have a collection of values to use as a SpanTerm clause. The collection size varies by query.
If the collection size was limited I could do:
var sn = q.SpanNear(snr => snr.Clauses(c => c.SpanTerm(), c => c.SpanTerm(), ...))
How do I do it dynamically (unknown clauses count)?

You can use this extension method:
public static class SpanNearQueryDescriptorExtensions
{
public static void SpanTermClauses<T>(this SpanNearQueryDescriptor<T> descriptor, Expression<Func<T, object>> field, string[] terms)
where T : class
{
descriptor.Clauses(terms
.Select(t => new Func<SpanQuery<T>, SpanQuery<T>>(query => query.SpanTerm(field, t)))
.ToArray());
}
}
Usage:
var terms = new[] {"term1", "term2"};
client.Search<Docuemnt>(s => s
.Query(q => q
.SpanNear(sp => sp
.Slop(12)
.SpanTermClauses(f => f.Title, terms))));
Hope it helps.

Related

using linq to find if a text field contains any string in a list

im running this in asp.net core v3.1
my question is similar to this question:
How to use Linq to check if a list of strings contains any string in a list
with the specific question relating to the first answer such that
filterTags = ["abc", "cd", "efg"]
var results = db.People
.Where(p => filterTags.Any(tag => p.Tags.Contains(tag)));
so basically saying
give me results from the db of all People
who's Tags field contains any of the filterTags
where Tags = a big text field populated by a bunch of space-delimited tags
This seems straightforward (esp since this has been written before)
but i get an error back
System.InvalidOperationException: The LINQ expression 'DbSet
.Where(p => __filterTags_0
.Any(tag => p.Tags.Contains(tag)))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync()
does anyone know what this means or what im doing wrong?
This is not possible with pure EF LINQ. You have to create helper which transforms your search list in Expression Tree.
public static class QueryExtensions
{
private static MethodInfo _containsMethodInfo = typeof(string).GetMethod("Contains")!;
public static IQueryable<T> FilterUsingContains<T>(this IQueryable<T> query, Expression<Func<T, string>> prop, IList<string> items)
{
if (items.Count == 0)
return query.Where(e => 1 == 2);
var param = prop.Parameters[0];
var predicate = items.Select(i =>
(Expression)Expression.Call(prop.Body, _containsMethodInfo, Expression.Constant(i, typeof(string))))
.Aggregate(Expression.OrElse);
var lambda = Expression.Lambda<Func<T, bool>>(predicate, param);
return query.Where(lambda);
}
}
Then you can use this extension in your queries
filterTags = ["abc", "cd", "efg"]
var results = db.People
.Where(p => p.Tags.AsQueryable().FilterUsingContains(t => t, filterTags).Any());
Here is a workaround for you:
using System.Linq;
string[] filterTags = {"abc", "cd", "efg"};
var results = db.People.Where(p => filterTags.Contains(p.Tags)).ToList();

Distinct/GroupBy in WhenAll result Async

I am writing a method in which i am using async prog.
var tasks = new List<Task<List<SomeClass>>>();
tasks.Add(this.Method1());
tasks.Add(this.Method2());
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
i want distinct records from this result. How to achieve that.
currently i have written
return results.SelectMany(s => s).GroupBy(x => x.Name).Select(x => x.FirstOrDefault()).ToList();
But i am not sure with SelectMany, will this give correct result.
SelectMany(s => s) is a "flatten" operation. It takes a sequence of sequences and flattens them to a single sequence.
The LINQ "distinct" operator is called Distinct. If your SomeClass overrides equality to be based on Name, then that's all you need:
return results.SelectMany(s => s).Distinct().ToList();
But if SomeClass doesn't define equality that way, you'll need to do another kind of distinct.
One option is to use the Distinct overload that takes an equality comparer. Then you can pass in an equality comparer that determines equality by Name. To do this, first define an equality comparer:
public sealed class NameEqualityComparer: IEqualityComparer<SomeClass>
{
public int GetHashCode(SomeClass obj) => EqualityComparer<string>.Default.GetHashCode(obj.Name);
public bool Equals(SomeClass x, SomeClass y) => EqualityComparer<string>.Default.Equals(x.Name, y.Name);
}
and then you can invoke the correct overload:
return results.SelectMany(s => s).Distinct(new NameEqualityComparer()).ToList();
I have a library that helps define custom comparers (properly handling all edge cases), which I prefer to use for things like this. With the Nito.Comparers library, you don't need to define a custom NameEqualityComparer; instead, you can define comparers in-line like this:
return results.SelectMany(s => s).Distinct(b => b.EquateBy(x => x.Name)).ToList();
or separately, if desired:
var comparer = EqualityComparerBuilder.For<SomeClass>().EquateBy(x => x.Name);
return results.SelectMany(s => s).Distinct(comparer).ToList();
A completely different option is to add a new "Distinct-By" operator that acts the way you want. This is part of MoreLINQ or you can add it yourself:
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> #this, Func<T, TKey> selector)
{
var keys = new HashSet<TKey>();
foreach (var item in #this)
{
if (keys.Add(selector(item)))
yield return item;
}
}
Then you can use the new operator like this:
return results.SelectMany(s => s).DistinctBy(x => x.Name).ToList();
All of these options are more efficient than grouping.

How can I add Sum from another Table in my Model?

So I have my View setup like this in the controller:
public ActionResult View(Guid projectID)
{
OnboardModel model = context.onboard_projectInfos.Where(x => x.projectID == projectID).Select(x =>
new OnboardModel()
{
propertymanagername = x.propertymanagername,
propertymanagercontactemail = x.propertymanagercontactemail,
date_modified = (DateTime)x.date_modified,
projectmanagercontactnumber = x.projectmanagercontactnumber,
Developer = x.onboard_projectCreate.Developer,
status1 = x.onboard_projectCreate.status1,
ProjectName = x.onboard_projectCreate.ProjectName
}).SingleOrDefault();
var pix = projectID.ToString();
context.onboard_BuildingInfos.Where(x => x.buildprojectID == pix).GroupBy(x => x.buildprojectID).Select(g => {
model.totalres = g.Sum(b => b.numberofres);
model.totalcom = g.Sum(b => b.numberofcommer);
});
return View(model);
}
Problem is grabbing the sum of numberofres and numberofcommer from BuildingInfos.
Using .Select gives me the error:
Error CS0411 The type arguments for method 'Queryable.Select(IQueryable, Expression>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
How to I write this LINQ statement correctly?
Thanks.
You cannot modify an object within a select (you can only create a new object). Further, you can't add new properties to an existing object.
We'll assume that OnboardModel defines the totalres and totalcom properties.
var query = context.onboard_BuildingInfos
.Where(x => x.buildprojectID == pix)
.GroupBy(x => x.buildprojectID);
foreach(var g in query)
{
model.totalres = g.Sum(b => b.numberofres);
model.totalcom = g.Sum(b => b.numberofcommer);
}

How to use Func with IQueryable that returns IOrderedQueryable

I'm doing some research about EF and came across a function that accepts
Func<IQueryable<Student>, IOrderedQueryable<Student>>
and just wondering how to call that function that accepts that kind of parameter?
imagine function is something like that, and you've got a property Id in Student class.
public static class Helper {
public static void Test(Func<IQueryable<Student>, IOrderedQueryable<Student>> param)
{
var test = 0;
}
}
then you could use it this way
var student = new List<Student>().AsQueryable();//non sense, just for example
Helper.Test(m => student.OrderBy(x => x.Id));
m => student.OrderBy(x => x.Id) is a
Func<IQueryable<Student>, IOrderedQueryable<Student>>
(IQueryable<student> as parameter, returning a IOrderedQueryable<Student>)
or just
Helper.Test(m => m.OrderBy(x => x.Id));
In fact this doesn't make much sense without a "real" function...
define a method.
public IOrderedQueryable<Student> OrderingMethod(IQueryable<Student> query)
{
return query.OrderBy(student => student.Name);
}
Now this assignment is legal:
Func<IQueryable<Student>, IOrderedQueryable<Student>> orderingFunc = this.OrderingMethod;
And now that you have it in a variable, it's easy to pass it to the method.
You could also do it all inline:
Func<IQueryable<Student>, IOrderedQueryable<Student>> orderingFunc =
query => query.OrderBy(student => student.Name);

How to create a collection of Expression<Func<T, TRelated>>?

I have a repository with the following method:
IEnumerable<T> FindAll<TRelated>(Specification<T> specification,
Expression<Func<T, TRelated>> fetchExpression);
I need to pass in more than one expression. I was thinking about changing the signature to:
IEnumerable<T> FindAll<TRelated>(Specification<T> specification,
IEnumerable<Expression<Func<T, TRelated>>> fetchExpression);
Is this possible?
How do I create an array, say, of expressions to pass into this method?
Currently I'm calling the method from my service layer like this:
var products = productRepository.FindAll(specification,p => p.Variants);
But I'd like to pass p => p.Variants and p => p.Reviews for example. And then in the repository I'd like to iterate through the expression and add them to a query.
For a bit of background on why I am doing this see Ben Foster's blog post on Eager loading with NHibernate.
You could use params to do it:
IEnumerable<T> FindAll(Specification<T> specification,
params Expression<Func<T, object>>[] fetchExpressions)
{
var query = GetQuery(specification);
foreach(var fetchExpression in fetchExpressions)
{
query.Fetch(fetchExpression);
}
return query.ToList();
}
You can call this like so:
var products = productRepository.FindAll(specification,
p => p.Variants, p => p.Reviews );
You can change your call to this:
var products = productRepository.FindAll(specification,
new [] { p => p.Variants,
p => p.Reviews });
But this will only work if the T is the same in both!

Resources