Insert a Dynamic into Elastic Document through nest - elasticsearch

var doc = new ESDoc {
Field1 = "test1",
Field2 = 3,
ExtraData = 'dynamic object',
Index = "myindex"
};
ElasticClient.Index(doc, s => s.Index(doc.Index));
This is in essence what I am trying to do. I have a document object, and I am wanting to add to it a dynamic object that allows us to through whatever customer specific data we want in there. I have no need to ever search or do any querying on it, I just need to hold it for CYA issues.
This results in ExtraData having a value_kind = 1.
I tried to JsonSerializer.Serialize the data and it came out in a triple escaped string.
I have seen people trying to create a entire document of dynamic data, and using a object cast, but I feel that isnt the answer here because I have a document that I want to add a dynamic object too.
NEST and Elaticsearch.Net 7.16.0

You can do this with the following:
You need to have a type to model the dynamic behaviour of the field. Now, you could specify it as dynamic on the ESDoc class, but you'll need an instance of a type to assign to it and Nest deserializes JSON to a Dictionary<string, object> for a dynamic member. Because of this, you might want to use the DynamicDictionary type in Elasticsearch.Net (a dependency of Nest) that is a dictionary with dynamic access behaviour
public class ESDoc
{
public string Field1 { get; set; }
public int Field2 { get; set; }
public DynamicDictionary ExtraData { get; set; }
}
Map ExtraData as object data type with enabled: false, so that it is not parsed and indexed
var createIndexResponse = client.Indices.Create("myindex", c => c
.Map<ESDoc>(m => m
.Properties(p => p
.Text(t => t
.Name(f => f.Field1)
)
.Number(t => t
.Name(f => f.Field2)
.Type(NumberType.Integer)
)
.Object<object>(o => o
.Name(f => f.ExtraData)
.Enabled(false)
)
)
)
);
Now index a doc, refresh the index, and search for it to ensure it deserializes as expected
dynamic extraData = new DynamicDictionary();
extraData.foo = "Foo";
extraData.bar = new DynamicDictionary
{
["baz"] = new DynamicValue("Baz")
};
var doc = new ESDoc
{
Field1 = "test1",
Field2 = 3,
ExtraData = extraData
};
client.Index(doc, s => s.Index("myindex"));
client.Indices.Refresh("myindex");
var searchResponse = client.Search<ESDoc>(s => s.Index("myindex"));
var firstDoc = searchResponse.Documents.First();
var baz = firstDoc.ExtraData["bar"]["baz"];
Console.WriteLine($"{baz}");

Related

TermsAggregation on Array

have a problem with an index field that stores a list of strings in the way of
["abc_def_fgh", "abc_def_fgh" ,"123_345_456"]
Im trying to use TermsAggregation to get
"abc_def_fgh" (2)
"123_345_456" (1)
But cant get it working, as it results the count for each of the terms (abc (2), def(2) , etc)
Any idea?
Many thanks
Try something like
var result = await client.SearchAsync<Document>(s => s
.Size(0)
.Aggregations(a => a
.Terms("tags", t => t.Field(f => f.Tags.Suffix("keyword")))));
foreach (var bucket in result.Aggregations.Terms("tags").Buckets)
{
System.Console.WriteLine($"Tag {bucket.Key}, doc count: {bucket.DocCount}");
}
public class Document
{
public string Id { get; set; }
public string[] Tags { get; set; } = { };
}
for these three documents in my index
new Document {Id = "1", Tags = new[]{"a","b"}
new Document {Id = "2", Tags = new[]{"a"}
new Document {Id = "3", Tags = new[]{"c"}
it will output
Tag a, doc count: 2
Tag b, doc count: 1
Tag c, doc count: 1
Hope that helps.
This sounds like a mapping issue. You need to make sure that you are using keyword opposed to text. You do not want the field to be analyzed when using aggregations.

Handling IGetResponse on Nest

I am using the Get API of Nest, but I don't know how to typecast the respone (IGetResponse) to the specific type of the document, something like this:
var response = client.Get<MyDocument>(documentId);
return response.Document(); // Or something like this that returns a MyDocument type
Also, is there a way to get the document for another unique field or only the Id is accepted?
response.Source holds document of type MyDocument.
As documentation says, you can use get api to get documents only by their ids.
You can tell elasticsearch to treat other field from document as Id.
With NEST you can do this as follows:
var indicesOperationResponse = client.CreateIndex(descriptor => descriptor
.Index(indexName)
.AddMapping<Document>(m => m.IdField(f => f.Path("uniqueValue"))));
client.Index(new Document{UniqueValue = "value1"});
var getResponse = client.Get<Document>(g => g.Id("value1"));
My document class:
public class Document
{
public string UniqueValue { get; set; }
}

Attribute based index hints change my results

I have this query that hasn't changed since I first got it working:
ISearchResponse<Series> response = await IndexManager.GetClient()
.SearchAsync<Series>(r => r
.Filter(f => f.Term<Role>(t => t.ReleasableTo.First(), Role.Visitor))
.SortDescending(ser => ser.EndDate)
.Size(1));
My IndexManager.GetClient() is simply responsible for setting up my connection to ElasticSearch, and ensuring that the indexes are built properly. The rest of the code gets the most recent article series that is releasable to the general public.
Inside the IndexManager I set up explicit index mapping, and when I did that I got results from my query every time. The code looked like this:
client.Map<Series>(m => m.Dynamic(DynamicMappingOption.Allow)
.DynamicTemplates(t => t
.Add(a => a.Name("releasableTo").Match("*releasableTo").MatchMappingType("string").Mapping(map => map.String(s => s.Index(FieldIndexOption.NotAnalyzed))))
.Add(a => a.Name("id").Match("*id").MatchMappingType("string").Mapping(map => map.String(s => s.Index(FieldIndexOption.NotAnalyzed))))
.Add(a => a.Name("services").Match("*amPm").MatchMappingType("string").Mapping(map => map.String(s => s.Index(FieldIndexOption.NotAnalyzed)))
.Match("*dayOfWeek").MatchMappingType("string").Mapping(map => map.String(s => s.Index(FieldIndexOption.NotAnalyzed))))
.Add(a => a.Name("urls").Match("*Url").MatchMappingType("string").Mapping(map => map.String(s => s.Index(FieldIndexOption.NotAnalyzed))))
));
While all well and good, doing this for every type we stored wasn't really going to scale well. So I made a conscious decision to use the attributes and map it that way:
// In IndexManager
client.Map<T>(m => m.MapFromAttributes());
// In the type definition
class Series
{
// ....
[DataMember]
[ElasticProperty(Index = FieldIndexOption.NotAnalyzed, Store = true)]
public HashSet<Role> ReleasableTo { get; set; }
// ....
}
As soon as I do this, I no longer get results. When I look at my indexes in Kibana, I see my 'releasableTo' field is not analyzed and it is indexed. However the query I wrote no longer works. If I remove the filter clause I get results, but I really need that to work.
What am I missing? How do I get my query to work again?
It appears that the ElasticSearch attributes to provide indexing hints don't know what to do with enums.
The problem turned out to be the fact that the Role type was an enumeration. The client.Map<Series>(m => m.MapFromAttributes()) call skipped that property. At run time, it dynamically maps the property to a string.
// In the type definition
class Series
{
// ....
[DataMember]
[ElasticProperty(Index = FieldIndexOption.NotAnalyzed, Store = true)]
public HashSet<Role> ReleasableTo { get; set; }
// ....
}
To get the field properly indexed I had to explicitly set it's type in the ElasticProperty attribute. Changing the code to this:
// In the type definition
class Series
{
// ....
[DataMember]
[ElasticProperty(Index = FieldIndexOption.NotAnalyzed, Type = FieldType.String, Store = true)]
public HashSet<Role> ReleasableTo { get; set; }
// ....
}
made my query work again. The moral of the story is that unless it's a primitive type, be explicit when setting the field type.

NEST mapping of Dictionary<string,object>

Im trying to use NEST and canĀ“t figure out how to use it together with this class
public class Metric {
public DateTime Timestamp { get; set; }
public Dictionary<string,object> Measurement { get; set; }
}
How do i use the new fluent mapping with a class like this?
Im planning to use i like this:
var mesurements = new Dictionary<string, object>();
mesurements["visits"] = 1;
mesurements["url"] = new string[] {"/help", "/about"};
connection.Index(new Metric() {
Timestamp = DateTime.UtcNow,
Measurement = mesurements
});
Will it be possible to write a query against the dictionary? If I wanted to get all Metrics from yesterday with a mesurenemt with a key name "visits", how will that look like ?
You don't have to use mapping, you can rely on elasticsearch's schemaless nature really well in this case.
The json serializer will write that out as:
{
"timestamp" : "[datestring]",
"measurement" : {
"visits" : 1,
"url" : [ "/help", "/about"]
}
}
You can query for the existence of the "measurement.visits" field like so using NEST.
var result = client.Search<Metric>(s=>s
.From(0)
.Size(10)
.Filter(filter=>filter
.Exists("measurement.visits")
)
);
result.Documents now hold the first 10 metrics with a visits key in the Measurement dictionary.
If you do want to explicitly map possible keys in that dictionary using the new fluent mapping:
var result = client.MapFluent<Metric>(m => m
.Properties(props => props
.Object<Dictionary<string,object>>(s => s
.Name(p => p.Measurement)
.Properties(pprops => pprops
.Number(ps => ps
.Name("visits")
.Type(NumberType.#integer)
)
.String(ps => ps
.Name("url")
.Index(FieldIndexOption.not_analyzed))
)
)
)
)
);
Remember that we haven't turned off dynamic mapping using this mapping so you can still inserts other keys into your dictionary without upsetting elasticsearch. Only now elasticsearch will know visits is an actual integer andwe dont want to analyze the url values.
since we are not using any typed accessors (The .Name() call is typed to Metric) .Object<Dictionary<string,object>> could be .Object<object> too.

IN and NOT IN with Linq to Entities (EF4.0)

This has been ruining my life for a few days now, time to ask...
I am using Entity Framework 4.0 for my app.
A Location (such as a house or office) has one or more facilities (like a bathroom, bedroom, snooker table etc..)
I want to display a checkbox list on the location page, with a checkbox list of facilities, with the ones checked that the location currently has.
My View Model for the facilities goes like this...
public class FacilityViewItem
{
public int Id { get; set; }
public string Name { get; set; }
public bool Checked { get; set; }
}
So when im passing the Location View Model to the UI, i want to pass a List<T> of facilities where T is of type FacilityViewItem.
To get the facilities that the location already has is simple - i make a query using Location.Facilities which returns an EntityCollection where T is of type Facility. This is because Facilities is a navigation property....
var facs = from f in location.Facilities
select new FacilityViewItem()
{
Id = f.FacilityId,
Name = f.Name,
Checked = true
};
So here is where my problem lies - i want the rest of the facilities, the ones that the Location does not have.
I have tried using Except() and Any() and Contains() but i get the same error.
Examples of queries that do not work...
var restOfFacilities = from f in ctx.Facilities
where !hasFacilities.Contains(f)
select new FacilityViewItem()
{
Id = f.FacilityId,
Name = f.Name
};
var restOfFacilities = ctx.Facilities.Except(facilitiesThatLocationHas);
var notFacs = from e in ctx.Facilities
where !hasFacilities.Any(m => m.FacilityId == e.FacilityId)
select new FacilityViewItem()
{
Id = e.FacilityId,
Name = e.Name
};
And the error i get with every implementation...
System.NotSupportedException was unhandled
Message=Unable to create a constant value of type 'Chapter2ConsoleApp.Facility'. Only primitive types ('such as Int32, String, and Guid') are supported in this context.
What am i overlooking here?
ironically enough i solved it in a matter of hours after i posted the question on here, after days of suffering.
The error is basically saying 'i dont know how to calculate what items are not included by comparing strongly typed objects. Give me a list of Ints or some simple types, and i can take care of it'.
So, first you need to get a list of the primary keys, then use that in the contains clause...
//get the primary key ids...
var hasFacilityIds = from f in hasFacilities
select f.FacilityId;
//now use them in the contains clause...
var restOfFacilities = from f in ctx.Facilities
where !hasFacilityIds.Contains(f.FacilityId)
select new FacilityViewItem()
{
Id = f.FacilityId,
Name = f.Name
};
The first query seems fine, but you need to compare the Ids:
var restOfFacilities = from f in ctx.Facilities
where !facs.Select(fac => fac.Id).Contains(f.Id)
select f;
I wanna see what's hasFacilities, anyway, as L2E shows, "Only primitive types ('such as Int32, String, and Guid') are supported in this context", so I suppose you must retrieve first the data and put into a collection of FacilityViewItem.
var restOfFacilities = ctx
.Facilities
.Where(f => !hasFacilities.Contains(f))
.Select(f => new { f.FacilityId, f.Name })
.ToList()
.Select(f => new FacilityViewItem {
Id = f.FacilityId,
Name = f.Name
});
var notFacs = ctx
.Facilities
.Where(e => !hasFacilities.Any(m => m.FacilityId == e.FacilityId))
.Select(e => new { e.FacilityId, e.Name })
.ToList()
.Select(e => new FacilityViewItem {
Id = e.FacilityId,
Name = e.Name
});
hope it helps

Resources