ElasticSearch.NET NEST search<T> url - c#

My correct index path is POST: /foo/_search but below code hits POST: /foo/bar/_search.
var node = new Uri("http://elasticsearch-server.com:9200");
var settings = new ConnectionSettings(node);
settings.DefaultIndex("foo");
var client = new ElasticClient(settings);
var response = client.Search<Bar>(s => s
.Query(q => q.Term(o => o.userName, "test"))
);
// POCO for response fields
public class Bar
{
public int userId { get; set; }
public string userName { get; set; }
public DateTime createdTime { get; set; }
}
Above code response returns below message;
Valid NEST response built from a successful low level call on POST: /foo/bar/_search
How can I set search path correctly?
Trial 1
When I omitted settings.DefaultIndex("foo"); line, it throws ArgumentException as below, but when I set DefaultIndex(), Search<T> uses T name as a second path.
ArgumentException: Index name is null for the given type and no default index is set. Map an index name using ConnectionSettings.MapDefaultTypeIndices() or set a default index using ConnectionSettings.DefaultIndex().
Trial 2
Refer to the documentation,
var settings = new ConnectionSettings(node)
.MapDefaultTypeIndices(m => m.Add(typeof(Bar), "foo"));
Above code returns same result in response.
Valid NEST response built from a successful low level call on POST: /foo/bar/_search

A large proportion of the Elasticsearch API exposed through NEST is in a strongly typed fashion, including .Search<T>(); with this endpoint, both "index" and "type" will be inferred from T, but sometimes you might want to set a different value to that which is inferred. In these cases, you can call additional methods on the search fluent API (or search object, if using the object initializer syntax) to override the inferred values
void Main()
{
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var connectionSettings = new ConnectionSettings(pool)
.DefaultIndex("foo");
var client = new ElasticClient(connectionSettings);
// POST http://localhost:9200/foo/bar/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.MatchAll()
);
// POST http://localhost:9200/foo/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.AllTypes()
.MatchAll()
);
// POST http://localhost:9200/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.AllTypes()
.AllIndices()
.MatchAll()
);
connectionSettings = new ConnectionSettings(pool)
.InferMappingFor<Bar>(m => m
.IndexName("bars")
.TypeName("barbar")
);
client = new ElasticClient(connectionSettings);
// POST http://localhost:9200/bars/barbar/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.MatchAll()
);
// POST http://localhost:9200/bars/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.AllTypes()
.MatchAll()
);
// POST http://localhost:9200/_all/barbar/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.AllIndices()
.MatchAll()
);
// POST http://localhost:9200/_search
// Will try to deserialize all _source to instances of Bar
client.Search<Bar>(s => s
.AllIndices()
.AllTypes()
.MatchAll()
);
}
public class Bar
{
public int userId { get; set; }
public string userName { get; set; }
public DateTime createdTime { get; set; }
}

You may add other parameters in your search lambda expression var response = client.Search<Bar>(s => s.Index("indexName").Query(q => q.Term(o => o.userName, "test")));

I was newbie in ElasticSearch and didn't know about _type.
I set identical _type name to POCO class name, and it works as I expected.
So we can say, {index}/{type} is what path expression is.

Related

Get a unique list (by key) from a list-in-list

I have a list of requests. Each request has many approvers. I want to go through all the requests and their approvers and get a list of unique approvers and their requests.
Here are sample models:
var requestsToProcess = await GetBatchOfApprovedRequestsAsyn(); // new List<RequestModel>();
public class RequestModel
{
public RequestModel()
{
ApproversList = new List<RequestApproverModel>();
}
public long Id { get; set; } // Key
public string Brief { get; set; }
public string Description { get; set; }
public List<RequestApproverModel> ApproversList { get; set; }
}
public class RequestApproverModel
{
public string Email { get; set; } // Key
public string FullName { get; set; }
}
I know how to get unique tuple from a list but don't understand if the target list is on an element of another list.
Basically the premise, is flatten and project, then groupby, then optionally project again.
Given
var requests= new List<RequestModel>()
{
new()
{
Id = 1,
ApproversList = new List<RequestApproverModel>()
{
new(){Email = "bob"},
new(){Email = "dole"}
}
},
new()
{
Id = 2,
ApproversList = new List<RequestApproverModel>()
{
new(){Email = "bob"},
new(){Email = "blerg"}
}
}
};
Example
var results =requests.SelectMany(request =>
request.ApproversList,
(request, approver) => new {request, approver})
.GroupBy(x => x.approver.Email )
.Select(x => new { Approver = x.Key, Requests = x.Select(y => y.request).ToList() });
foreach (var item in results)
{
Console.WriteLine(item.Approver);
foreach (var request in item.Requests)
Console.WriteLine(" " + request.Id);
}
Output
bob
1
2
dole
1
blerg
2
The two complementary methods you need from LINQ are SelectMany, which unpacks a list-of-lists to a list, and GroupBy, which packs a list to a list-of-lists (you need to go from a-of-b to b-of-a)
var result = someRequestModels
.SelectMany(rm => rm.ApproversList, (rm, ram) => new { RM = rm, RamEmail = ram.Email })
.GroupBy(at => at.RamEmail, at => at.RM);
The SelectMany is like a nested pair of foreach
foreach(var rm in someRequestModels)
foreach(var ram in rm.ApproversList)
flatlist.Add(new { rm, ram});
This has turned the list of lists into a single list, repeating the RequestModel over and over per RequestApproverModel. You can then run a GroupBy of approver Email which takes every unique email in the flattened list and puts together a list of list of RequestModels. In non LINQ pseudocode it'd look something like:
foreach(var rmRamPair In flatlist)
grouping[rmRamPair.Email].Add(rmRamPair.Rm);
This produces an IGrouping which is something like a list of lists, where each entry has a Key, a string of the approver's email and is an enumerable of all the requestmodels they have, so eg
foreach(var x in result){
Console.WriteLine($"approver with email of {x.Key} has cases:";
foreach(var rm in x)
Console.WriteLine($"id is '{rm.Id}' and Brief is '{rm.Brief}'");
}
If it makes you more comfortable, you can call ToDictionary(x => x.Key, x => x.ToList()) on the result and you'll get a Dictionary<string, List<RequestModel>> out, the email being the key and and list of requestmodels being the value
If you want the whole RequestApproverModel, not just the email it might be a bit more tricky. It's easy if you've reused instances of RAM so if there is literally only one object in memory that is "bob#mail.com" and that object is present on a couple of different requests:
var ram = new RequestApproverModel{ Email = "bob#mail.com" };
var r1 = new RequestModel();
r1.ApproversList.Add(ram);
var r2 = new RequestModel();
r2.ApproversList.Add(ram);
Here the instance is the same one; you can just group by it instead of the email.
If you've ended up with objects that look the same but are at different memory addresses:
var r1 = new RequestModel();
r1.ApproversList.Add(new RequestApproverModel{ Email = "bob#mail.com" });
var r2 = new RequestModel();
r2.ApproversList.Add(new RequestApproverModel{ Email = "bob#mail.com" });
Then the standard implementation of Equals and GetHashcode(inherited from object) is useless because it's based on the memory stress where the instances live.
Your RequestModel class will instead need to implement Equals and GetHashcode that report equality based on Email, otherwise grouping by the whole RequestModel won't work out

Automapper adding valueTransformer to the "dictionary to object" mapper

I am trying to map a Dictionary to a POCO with the builtin mapper and apply an additional transformation on strings with a global ValueTransformer:
class MyPoco
{
public string StringValue { get; set; }
}
The mapper call:
var source = new Dictionary<string, object>
{
{ "StringValue", "abc" }
};
var mapper = new MapperConfiguration(cfg =>
{
cfg.ValueTransformers.Add<string>(dest => dest + "!!!");
})
.CreateMapper();
var poco = mapper.Map<MyPoco>(source);
Assert.Equal("abc!!!", poco.StringValue);
After the mapping call the poco.StringValue equals to 'abc' and the configured ValueTransfomer is never called. Am I missing something?

Filtering nested lists with nullable property

Say I have the following class structures
public class EmailActivity {
public IEnumerable<MemberActivity> Activity { get; set; }
public string EmailAddress { get; set; }
}
public class MemberActivity {
public EmailAction? Action { get; set; }
public string Type { get; set; }
}
public enum EmailAction {
None = 0,
Open = 1,
Click = 2,
Bounce = 3
}
I wish to filter a list of EmailActivity objects based on the presence of a MemberActivity with a non-null EmailAction matching a provided list of EmailAction matches. I want to return just the EmailAddress property as a List<string>.
This is as far as I've got
List<EmailAction> activityTypes; // [ EmailAction.Open, EmailAction.Bounce ]
List<string> activityEmailAddresses =
emailActivity.Where(
member => member.Activity.Where(
activity => activityTypes.Contains(activity.Action)
)
)
.Select(member => member.EmailAddress)
.ToList();
However I get an error message "CS1503 Argument 1: cannot convert from 'EmailAction?' to 'EmailAction'"
If then modify activityTypes to allow null values List<EmailAction?> I get the following "CS1662 Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type".
The issue is the nested .Where it's returning a list, but the parent .Where requires a bool result. How would I tackle this problem?
I realise I could do with with nested loops however I'm trying to brush up my C# skills!
Using List.Contains is not ideal in terms of performance, HashSet is a better option, also if you want to select the email address as soon as it contains one of the searched actions, you can use Any:
var activityTypes = new HashSet<EmailAction>() { EmailAction.Open, EmailAction.Bounce };
List<string> activityEmailAddresses =
emailActivity.Where(
member => member.Activity.Any(
activity => activity.Action.HasValue &&
activityTypes.Contains(activity.Action.Value)
)
)
.Select(activity => activity.EmailAddress)
.ToList();
You want to use All or Any depends if you want each or at least one match...
HashSet<EmailAction> activityTypes = new HashSet<EmailAction> { EmailAction.None };
var emailActivity = new List<EmailActivity>
{
new EmailActivity { Activity = new List<MemberActivity>{ new MemberActivity { Action = EmailAction.None } }, EmailAddress = "a" },
new EmailActivity { Activity = new List<MemberActivity>{ new MemberActivity { Action = EmailAction.Click } }, EmailAddress = "b" }
};
// Example with Any but All can be used as well
var activityEmailAddresses = emailActivity
.Where(x => x.Activity.Any(_ => _.Action.HasValue && activityTypes.Contains(_.Action.Value)))
.Select(x => x.EmailAddress)
.ToArray();
// Result is [ "a" ]

How to use mapper.Map inside MapperConfiguration of AutoMapper?

I need to map an object to another one using AutoMapper. The tricky question is how can I access an instance of the mapper (instance of IMapper) inside of the mapping configuration or inside of a custom type converter?
The code below does not work, however it is an example of what I would like to achieve - please notice the mapper.Map calls and assume that mappings Customer => CustomerDto and Customer => DetailedCustomerDto are defined.
var config = new MapperConfiguration(
cfg => cfg.CreateMap<Order, OrderDto>()
.ForMember(dst => dst.Customer, src => src.ResolveUsing(o => {
return o.Type == 1
? mapper.Map<Customer, CustomerDto>(o.Customer)
: mapper.Map<Customer, DetailedCustomerDto>(o.Customer)
})
);
The client part is:
var mapper = config.CreateMapper();
var orderDto = mapper.Map<Order, OrderDto>(order);
The simplified version of objects I want to map is:
public class Order
{
public int Type { get; set; }
public Customer Customer { get; set; }
}
public class Customer
{
public long Id { get; set; }
public string Name { get; set; }
}
public class OrderDto
{
public CustomerDto Customer { get; set; }
}
public class CustomerDto
{
public long Id { get; set; }
}
public class DetailedCustomerDto : CustomerDto
{
public string Name { get; set; }
}
As you see from the code above, based on the value of Order.Type, the mapper should map the property Order.Customer to different targets. As one target (DetailedCustomerDto) inherits from the other one (CustomerDto) it becomes a bit tricky.
Please notice that usage of the obsolete and deprecated static method Mapper.Map is NOT an option.
As of AutoMapper 8.0 and up
The answer below for 5.1.1 still applies, but note that the use of ResolveUsing has been replaced with an overload of MapFrom, but the signature has otherwise remained consistent.
As of AutoMapper 5.1.1
You can get to the mapper using another overload of ResolveUsing with four parameters, fourth of which is ResolutionContext (context.Mapper):
var config = new MapperConfiguration(
cfg => {
cfg.CreateMap<Customer, CustomerDto>();
cfg.CreateMap<Customer, DetailedCustomerDto>();
cfg.CreateMap<Order, OrderDto>()
.ForMember(dst => dst.Customer, src => src.ResolveUsing((order, orderDto, i, context) => {
return order.Type == 1
? context.Mapper.Map<Customer, CustomerDto>(order.Customer)
: context.Mapper.Map<Customer, DetailedCustomerDto>(order.Customer);
}));
});
var orderTypeOne = new Order();
orderTypeOne.Type = 1;
orderTypeOne.Customer = new Customer() {
Id = 1
};
var dto = config.CreateMapper().Map<Order, OrderDto>(orderTypeOne);
Debug.Assert(dto.Customer.GetType() == typeof (CustomerDto));
var orderTypeTwo = new Order();
orderTypeTwo.Type = 2;
orderTypeTwo.Customer = new Customer() {
Id = 1
};
dto = config.CreateMapper().Map<Order, OrderDto>(orderTypeTwo);
Debug.Assert(dto.Customer.GetType() == typeof (DetailedCustomerDto));
Prior to AutoMapper 5.1.1
You can get to the mapper using another overload of ResolveUsing with two parameters, first of which is ResolutionResult (result.Context.Engine.Mapper):
var config = new MapperConfiguration(
cfg => {
cfg.CreateMap<Customer, CustomerDto>();
cfg.CreateMap<Customer, DetailedCustomerDto>();
cfg.CreateMap<Order, OrderDto>()
.ForMember(dst => dst.Customer, src => src.ResolveUsing((result, order) => {
return order.Type == 1
? result.Context.Engine.Mapper.Map<Customer, CustomerDto>(order.Customer)
: result.Context.Engine.Mapper.Map<Customer, DetailedCustomerDto>(order.Customer);
}));
});
var orderTypeOne = new Order();
orderTypeOne.Type = 1;
orderTypeOne.Customer = new Customer() {
Id = 1
};
var dto = config.CreateMapper().Map<Order, OrderDto>(orderTypeOne);
Debug.Assert(dto.Customer.GetType() == typeof (CustomerDto));
var orderTypeTwo = new Order();
orderTypeTwo.Type = 2;
orderTypeTwo.Customer = new Customer() {
Id = 1
};
dto = config.CreateMapper().Map<Order, OrderDto>(orderTypeTwo);
Debug.Assert(dto.Customer.GetType() == typeof (DetailedCustomerDto));
In addition to Evk's great answer, which helped me, if you need to do a mapping inside a mapping inside a config/profile that requires a custom constructor (i.e. the type has no default constructor), the following will work in v5.2.0:
CreateMap<Models.Job, Models.API.Job>(MemberList.Source);
CreateMap<StaticPagedList<Models.Job>, StaticPagedList<Models.API.Job>>()
.ConstructUsing((source, context) => new StaticPagedList<Models.API.Job>(
context.Mapper.Map<List<Models.Job>, List<Models.API.Job>>(source.ToList()),
source.PageNumber,
source.PageSize,
source.TotalItemCount));
In this example I'm mapping the X.PagedList custom collection type of one object type onto an equivalent collection of another object type. The first parameter to the lamdba expression is your source object, the second is your ResolutionContext from which you can access a mapper instance to map from.
I'm using Automapper 9 and the answers above didn't work for me.
Then for resolve my problem that is like yours I use .afterMap, like that:
public class AutoMapperOrder : Profile
{
public AutoMapperOrder()
{
CreateMap<Customer, CustomerDto>()
//...
CreateMap<Customer, DetailedCustomerDto>()
//...
CreateMap<Order, OrderDto>()
.AfterMap((src, dest, context) => {
dest.Customer = src.Type == 1
? context.Mapper.Map<Customer, CustomerDto>(src.Customer)
: context.Mapper.Map<Customer, DetailedCustomerDto>(src.Customer)
}
}
}
}
I hope to help somebody.

MongoDB Text Search with projection

Using MongoDB with C# and driver 2.0, I am trying to do the following:
Text search
Sort the hits by text search score
Project BigClass to SmallClass
Here is a (simplified version of) the classes:
class BigClass
{
[BsonIgnoreIfDefault]
public ObjectId _id { get; set; }
public string Guid { get; set; }
public string Title { get; set; }
public DateTime CreationTime { get; set; }
// lots of other stuff
[BsonIgnoreIfNull]
public double? TextMatchScore { get; set; } // Temporary place for the text match score, for sorting
}
class SmallClass
{
[BsonIgnoreIfDefault]
public ObjectId _id { get; set; }
public string Title { get; set; }
[BsonIgnoreIfNull]
public double? TextMatchScore { get; set; } // Temporary place for the text match score, for sorting
}
If I do a text search, it is pretty straightforward:
var F = Builders<BigClass>.Filter.Text("text I am looking for");
var Result = MongoDriver.Find(F).ToListAsync().Result;
If I want to sort by the score of the text search, it's a bit more messy (and very POORLY documented):
var F = Builders<BigClass>.Filter.Text("text I am looking for");
var P = Builders<BigClass>.Projection.MetaTextScore("TextMatchScore");
var S = Builders<BigClass>.Sort.MetaTextScore("TextMatchScore");
var Result = MongoDriver.Find(F).Project<BigClass>.Sort(S).ToListAsync().Result;
Essentially it requires me to add a field in the class (TextMatchScore) to hold the result.
If I want to get the data, without sorting and project it to SmallClass, it is straightforward:
var F = Builders<BigClass>.Filter.Text("text I am looking for");
var P = Builders<BigClass>.Projection.Include(_ => _.id).Include(_ => _.Title);
var Result = MongoDriver.Find(F).Project<SmallClass>(P).ToListAsync().Result;
Now if "I want it all", that's where problem arises:
var F = Builders<BigClass>.Filter.Text("text I am looking for");
var P = Builders<BigClass>.Projection.MetaTextScore("TextMatchScore").Include(_ => _.id).Include(_ => _.Title).Include(_ => _.TextMatchScore);
var S = Builders<BigClass>.Sort.MetaTextScore("TextMatchScore");
var Result = MongoDriver.Find(F).Project<SmallClass>.Sort(S).ToListAsync().Result;
I get an exception:
Message = "QueryFailure flag was true (response was { \"$err\" : \"Can't canonicalize query: BadValue must have $meta projection for all $meta sort keys\", \"code\" : 17287 })."
As expected, the error is not documented anywhere as the Mongo guys expect users to self-document everything.
If I make the projection to 'BigClass', there is no problem, the code runs and just fills in the right fields.
If you google that text with C#, the posts you find are mine when I was trying to figure out the text search, which is also poorly documented.
So when we combine projection, text search and sorting, there doesn't seem to be any example anywhere and I just can't get it to work.
Does anyone know the reason for that problem?
This works for me:
var client = new MongoClient();
var db = client.GetDatabase("test");
var col = db.GetCollection<BigClass>("big");
await db.DropCollectionAsync(col.CollectionNamespace.CollectionName);
await col.Indexes.CreateOneAsync(Builders<BigClass>.IndexKeys.Text(x => x.Title));
await col.InsertManyAsync(new[]
{
new BigClass { Title = "One Jumped Over The Moon" },
new BigClass { Title = "Two went Jumping Over The Sun" }
});
var filter = Builders<BigClass>.Filter.Text("Jump Over");
// don't need to Include(x => x.TextMatchScore) because it's already been included with MetaTextScore.
var projection = Builders<BigClass>.Projection.MetaTextScore("TextMatchScore").Include(x => x._id).Include(x => x.Title);
var sort = Builders<BigClass>.Sort.MetaTextScore("TextMatchScore");
var result = await col.Find(filter).Project<SmallClass>(projection).Sort(sort).ToListAsync();
I removed the include of the TextMatchScore. It still comes back, because it was included by the MetaTextScore("TextMatchScore").
Documentation is a work in progress. We tackle the major use cases first as those hit the most people. This use case isn't that common and hasn't been documented. We certainly accept pull requests, both for code and documentation. Also, feel free to file a documentation ticket at jira.mongodb.org under the CSHARP project.
Solution which works in MongoDB.Driver 2.x is as follows. What is important is to not do Include in Projection, as it will erase default one, (or remember to add proper projection)
Query:
{
"find":"SoceCollection",
"filter":{
"$text":{
"$search":"some text to search"
}
},
"sort":{
"TextScore":{
"$meta":"textScore"
}
},
"projection":{
"TextScore":{
"$meta":"textScore"
},
"_id":0,
"CreatedDate":0
},
"limit":20,
"collation":{
"locale":"en",
"strength":1
} ...
CODE
var sort = Builders<BigModel>.Sort.MetaTextScore(nameof(LightModel.TextScore));
var projection = Builders<BigModel>.Projection
.MetaTextScore(nameof(LightModel.TextScore))
.Exclude(x => x.Id)
.Exclude(x => x.CreatedDate);
return await Collection()
.Find(filter, new FindOptions { Collation = new Collation("en", strength: CollationStrength.Primary) })
.Project<LightModel>(projection)
.Sort(sort)
.Limit(20)
.ToListAsync();

Categories