Translate ElasticSearch query to Nest c# - elasticsearch

I need some help in creating an AggregationDictionary from the following elasticsearch query
GET organisations/_search
{
"size": 0,
"aggs": {
"by_country": {
"nested": {
"path": "country"
},
"aggs": {
"by_country2": {
"filter": {
"bool": {
"must": [
{
"term": {
"country.isDisplayed": "true"
}
}
]
}
},
"aggs": {
"by_country3": {
"terms": {
"field": "country.displayName.keyword",
"size": 9999
}
}
}
}
}
}
}
}
I managed to write this horrible piece of code which I am pretty sure it is wrong, I am totally new to this.
AggregationDictionary aggs = new AggregationDictionary()
{
{
"countries_step1",
new NestedAggregation("countries_step1")
{
Path = "country",
Aggregations = new AggregationDictionary()
{
{
"countries_step2",
new FilterAggregation("countries_step2")
{
Filter = new BoolQuery
{
Must = new QueryContainer[] {
new NestedQuery
{
Query = new TermQuery
{
Field = "country.isDisplayed",
Value = true
}
}
}
},
Aggregations = new AggregationDictionary
{
{
"countries_step3",
new TermsAggregation("countries_step3")
{
Field = "country.displayName.keyword",
Size = 9999
}
}
}
}
}
}
}
}
};
Can someone tell me if I am in the correct direction? I am using Nest 6.6.0. Is there any tool that helps with these translations?

What you have so far is pretty solid, but when you try to execute this aggregation with the following call
var searchAsync = await client.SearchAsync<Document>(s => s.Size(0).Aggregations(aggs));
you will get this error
{
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "query malformed, empty clause found at [14:22]"
}
],
"type" : "illegal_argument_exception",
"reason" : "query malformed, empty clause found at [14:22]"
},
"status" : 400
}
Checking request which was sent to elasticsearch give us the answer why it happened
{
"aggs": {
"countries_step1": {
"aggs": {
"countries_step2": {
"aggs": {
"countries_step3": {
"terms": {
"field": "country.displayName.keyword",
"size": 9999
}
}
},
"filter": {}
}
},
"nested": {
"path": "country"
}
}
},
"size": 0
}
filter clause is empty, this is because you tried to used nested query but you didn't pass path parameter. We don't need nested query here (as shown in your example query), we can simplify the whole query to
var aggs = new AggregationDictionary()
{
{
"countries_step1",
new NestedAggregation("countries_step1")
{
Path = "country",
Aggregations = new AggregationDictionary()
{
{
"countries_step2",
new FilterAggregation("countries_step2")
{
Filter = new BoolQuery
{
Must = new QueryContainer[]
{
new TermQuery
{
Field = "country.isDisplayed",
Value = true
}
}
},
Aggregations = new AggregationDictionary
{
{
"countries_step3",
new TermsAggregation("countries_step3")
{
Field = "country.displayName.keyword",
Size = 9999
}
}
}
}
}
}
}
}
};
Now we have a valid request sent to elasticsearch.
There are a couple of things we can improve here:
1. Remove unnecessary bool query
Filter = new BoolQuery
{
Must = new QueryContainer[]
{
new TermQuery
{
Field = "country.isDisplayed",
Value = true
}
}
},
to
Filter =
new TermQuery
{
Field = "country.isDisplayed",
Value = true
},
2. Replace string field names
Usually, when doing calls from .Net there is some kind of POCO type which is helping us with writing strongly-typed requests to elasticsearch which helps us managing clean code and refactoring. With this, we can change field definition from
"country.displayName.keyword"
to
Infer.Field<Document>(f => f.Country.FirstOrDefault().DisplayName.Suffix("keyword"))
my types definition
public class Document
{
public int Id { get; set; }
[Nested]
public List<Country> Country { get; set; }
}
public class Country
{
public bool IsDisplayed { get; set; }
public string DisplayName { get; set; }
}
3. Consider using a fluent syntax
With NEST you can write queries in two ways: using object initializer syntax (which you did) or with help of fluent syntax. Have a look. Trying to write above query with the fluent syntax you will get something like
var searchResponse = await client.SearchAsync<Document>(s => s
.Size(0)
.Aggregations(a => a.Nested("by_country", n => n
.Path(p => p.Country)
.Aggregations(aa => aa
.Filter("by_country2", f => f
.Filter(q => q
.Term(t => t
.Field(field => field.Country.FirstOrDefault().IsDisplayed)
.Value(true)))
.Aggregations(aaa => aaa
.Terms("by_country3", t => t
.Field(field => field.Country.FirstOrDefault().DisplayName.Suffix("keyword"))
.Size(9999)
)))))));
which I find a little bit easier to follow and write, maybe it will be better for you as well.
As a final note, have a look into docs and check how you can debug your queries.
Hope that helps.

Related

How to query elastic search with Hashmap

I would like to query the Elastic Search with map of values and retrieve the documents.
Example:
I have indexed the below two documents
1. {
"timestamp": 1601498048,
"props": {
"cp1": "cv1",
"cp2": "cv2"
}
}
2. {
"timestamp": 1601498098,
"props": {
"key1": "v1",
"key2": "v2"
}
}
So, I wanted to query with the entire map values props with
"props"
{
"cp1": "cv1",
"cp2": "cv2"
}
and return documents only for the entired matched map values. So in this case the result would be only first document, since it matched the given props.
I can able to query with only single map value like below , but need to search for entire map.
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"bool" : {
"must" : [
{
"terms" : {
"customProperties.cp1.keyword" : [ "cv1" ]
}
}
]
}
}
}
'
So how we query for entire map props and return documents only if all map key-values matched.
Update
Mainly I need a QueryBuilder to search with map of values. I could do for set of values like below
val sampleSet = setOf("foo", "bar")
val query = NativeSearchQueryBuilder()
.withQuery(
QueryBuilders.termsQuery(
"identifiers.endpointId.keyword", sampleSet)
)
.build()
I need QueryBuilder to search with map of values in the ES index and return document only if entire map values matches.
Suggestions please.
you must apply double match clausule.
{
"query": {
"bool": {
"must": [
{
"match": {
"props.cp1": "cv1"
}
},
{
"match": {
"props.cp2": "cv2"
}
}
]
}
}
}
Or Term.
{
"query": {
"bool": {
"must": [
{
"term": {
"props.cp1.keyword": "cv1"
}
},
{
"term": {
"props.cp2.keyword": "cv2"
}
}
]
}
}
}
This worked. I just looped through the queryBuilder with map values props.
val builder = QueryBuilders.boolQuery()
for (prop in props) {
builder.must(QueryBuilders.matchQuery("customProperties.${prop.key}", prop.value))
}
val query = NativeSearchQueryBuilder().withQuery(builder)
println("results + $queryForList(query)")
passed query to this function
internal fun queryForList(query: NativeSearchQuery): List<DocumentType> {
val resp = searchOperations.search(query, type, IndexCoordinates.of(indexName))
return resp.searchHits.map { it.content }
}

Elasticsearch bad request on mapping put method

I m trying to create an index in Elastic Search and to add analyzer and maaping to handle special characters like # to search on Email field. Here is my code
Analyzer.txt
{
"analysis": {
"analyzer": {
"email_search": {
"type": "custom",
"tokenizer": "uax_url_email",
"filter": [ "lowercase", "stop" ]
}
}
}
Mapping.txt
{"users":
{
"properties": {
"email": {
"type": "string",
"analyzer": "email_search"
}
}
}
CreatedIndex
string _docuementType ="users";
string _indexName="usermanager";
public bool Index(T entity)
{
SetupAnalyzers();
SetupMappings();
var indexResponse = _elasticClient.Index(entity, i => i.Index(_indexName)
.Type(_docuementType)
.Id(entity.Id));
return indexResponse.IsValid;
}
SetupAnalyzer
protected void SetupAnalyzers()
{
if (!_elasticClient.IndexExists(_indexName).Exists)
{
_elasticClient.CreateIndex(_indexName);
}
using (var client = new System.Net.WebClient())
{
string analyzers = File.ReadAllText("analyzers.txt");
client.UploadData("http://localhost:9200/usermanager/_settings", "PUT", Encoding.ASCII.GetBytes(analyzers));
}
}
SetupMappings
protected void SetupMappings()
{
using (var client = new System.Net.WebClient())
{
var mappings = File.ReadAllText("mappings.txt");
client.UploadData("http://localhost:9200/usermanager/users/_mapping", "PUT", Encoding.ASCII.GetBytes(mappings));
}
}
But getting error on SetupMappings method
The remote server returned an error: (400) Bad Request.
Elastic version is 1.7.5
Nest version is 5.5
The problem is
var indexResponse = _elasticClient.Index(entity, i => i.Index(_indexName)
.Type(_docuementType)
.Id(entity.Id));
You're indexing documents before setting up the mapping and analyzers; in this case, Elasticsearch will automatically create the index, infer a mapping from the first document it sees and string properties will be mapped as analyzed string fields using the standard analyzer.
To fix this, you should create the index, analyzers and mappings before indexing any documents. You can also disable auto index creation if you need to, in elasticsearch.yml config. It depends on your use case as to whether this is might be a good idea; a search use case with known indices and explicit mappings is a case where you might consider disabling it.
So, the procedural flow would be something like
void Main()
{
var indexName = "usermanager";
var typeName = "users";
var settings = new ConnectionSettings(new Uri("http://localhost:9200"))
.MapDefaultTypeNames(d => d
// map the default index to use for the User type
.Add(typeof(User), typeName)
)
.MapDefaultTypeIndices(d => d
// map the default type to use for the User type
.Add(typeof(User), indexName)
);
var client = new ElasticClient(settings);
if (!client.IndexExists(indexName).Exists)
{
client.CreateIndex(indexName, c => c
.Analysis(a => a
.Analyzers(aa => aa
.Add("email_search", new CustomAnalyzer
{
Tokenizer = "uax_url_email",
Filter = new [] { "lowercase", "stop" }
})
)
)
.AddMapping<User>(m => m
.Properties(p => p
.String(s => s
.Name(n => n.Email)
.Analyzer("email_search")
)
)
)
);
}
client.Index(new User { Email = "me#example.com" });
}
public class User
{
public string Email { get; set; }
}
which will send the following requests (assuming index doesn't exist)
HEAD http://localhost:9200/usermanager
POST http://localhost:9200/usermanager
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"email_search": {
"tokenizer": "uax_url_email",
"filter": [
"lowercase",
"stop"
],
"type": "custom"
}
}
}
}
},
"mappings": {
"users": {
"properties": {
"email": {
"analyzer": "email_search",
"type": "string"
}
}
}
}
}
POST http://localhost:9200/usermanager/users
{
"email": "me#example.com"
}
NOTE: You will need to delete the index and recreate, in order to change the mapping. It's a good idea to use aliases to interact with indices, so that you can iterate on mappings whilst developing.

Can't nest terms aggregations more than two deep without further aggs being ignored?

I'm querying ElasticSearch using the Nest library for C#, to fetch graph data with multiple pivots. Each pivot is a nested TermsAggregation on a query, and everything works fine with one or two pivots. Once I get to three pivots, though, the SearchRequest object won't generate further aggregations.
The code to build the aggregations looks like this:
TermsAggregation topTermAgg = null;
TermsAggregation currentAgg = null;
foreach (var pivotName in activePivots)
{
newTermAgg = new TermsAggregation("pivot")
{
Field = pivot.ToString().ToLower()
};
if (topTermAgg == null)
{
topTermAgg = newTermAgg;
}
else
{
currentAgg.Aggregations = newTermAgg;
}
currentAgg = newTermAgg;
}
The SearchRequest itself is pretty straightforward:
var searchRequest = new SearchRequest(Indices.Index("a", "b", "c"))
{
Size = 0,
Aggregations = topTermAgg,
Query = query,
};
Unfortunately, the SearchRequest for 3 or more pivots, when converted to string, looks like this (via nestClient.Serializer.SerializeToString(searchRequest)):
{
"size": 0,
"query": {
"bool": <Fairly complex query, that works fine. It's the aggregation that has the problem.>
},
"aggs": {
"pivot": {
"terms": {
"field": "pivot1"
},
"aggs": {
"pivot": {
"terms": {
"field": "pivot2"
}
}
}
}
}
}
When I inspect the searchRequest object in the debugger, it quite definitely has 3 or more aggregations. What's going on here, and how can I get 3 or more nested terms aggregations to work properly?
I am using Nest version 5.01.
This must be related to the way in which you're building up the nested aggregations. Arbitrarily deep nested aggregations can be built with the client. Here's an example of a three deep nested aggregation
client.Search<Question>(s => s
.Aggregations(a => a
.Terms("top", ta => ta
.Field("top_field")
.Aggregations(aa => aa
.Terms("nested_1", nta => nta
.Field("nested_field_1")
.Aggregations(aaa => aaa
.Terms("nested_2", nnta => nnta
.Field("nested_field_3")
)
)
)
)
)
)
);
which serializes to
{
"aggs": {
"top": {
"terms": {
"field": "top_field"
},
"aggs": {
"nested_1": {
"terms": {
"field": "nested_field_1"
},
"aggs": {
"nested_2": {
"terms": {
"field": "nested_field_3"
}
}
}
}
}
}
}
}
You can also add values to AggregationDictionary directly
var request = new SearchRequest<Question>
{
Aggregations = new AggregationDictionary
{
{ "top", new TermsAggregation("top")
{
Field = "top_field",
Aggregations = new AggregationDictionary
{
{ "nested_1", new TermsAggregation("nested_1")
{
Field = "nested_field_1",
Aggregations = new AggregationDictionary
{
{ "nested_2", new TermsAggregation("nested_2")
{
Field = "nested_field_2"
}
}
}
}
}
}
}
}
}
};
client.Search<Question>(request);
is the same as the previous request. You can shorten this even further to
var request = new SearchRequest<Question>
{
Aggregations = new TermsAggregation("top")
{
Field = "top_field",
Aggregations = new TermsAggregation("nested_1")
{
Field = "nested_field_1",
Aggregations = new TermsAggregation("nested_2")
{
Field = "nested_field_2"
}
}
}
};
client.Search<Question>(request);
I got my code working by constructing the aggregation from the bottom-up, rather than from the top-down.
var terminalAggregation = <some aggregation. In my code, there's a lowest aggregation that's different from the rest. For the code I presented, you could just build the lowest pivot.>
TermsAggregation topTermAgg = null;
activePivots.Reverse();
foreach (var pivotName in activePivots)
{
newTermAgg = new TermsAggregation("pivot")
{
Field = pivot.ToString().ToLower(),
Aggregations = topTermAgg ?? terminalAggregation
};
topTermAgg = newTermAgg;
}
This looks like a bug in the Nest library; there are different classes like AggregationBase and BucketAggregationBase and AggregationDictionary that are all assignable to the "Aggregations" property, but it seems like there's some subtle flaw after the second assignment when you do this recursively.
The documentation is also not up-to-date: it claims that you can create an AggregationDictionary yourself, but since AggregationDictionary doesn't have a public Add() method, I really can't. Nor can I use the C#'s {}-after-insantiation syntax to populate its properties – again, because Add() is not public.

Elasticsearch/Nest - Combining multiple range queries (OIC syntax)

ES newbie here (transitioning over from the SolrNet world, so I am looking for pointers with a similar syntax)
Can anyone help with formulating a query that might look something like the code snippet below without the lambdas. Unfortunately there are very few examples in the plain old OI syntax and I have been stuck....
How would I formulate a set of boolean range queries, in solrnet I would do something like this to obtain items that fall in a certain price band:
List<ISolrQuery> queryList = new List<ISolrQuery>();
double[] priceList = GetPrices(..) //double array
for (int i = 1; i <= 10; i++)
{
queryList.Add(new solr.Query(new SolrQueryByRange<double>("price", priceList[i] * 0.95, priceList[i] * 1.15));
}
var results = _solr.Query(new SolrMultipleCriteriaQuery(queryList), new QueryOptions
{
Rows = 50,
Fields = new[] { "Item", "Created", "Price" },
});
Could anyone help with equivalent Elasticsearch/Nest query?
Something like the following
void Main()
{
var client = new ElasticClient(connection: new InMemoryConnection());
// prices from your GetPrices method...
var prices = new[] { 1d, 2d };
var searchRequest = new SearchRequest<Document>("index", "document")
{
Fields = new List<Nest.PropertyPathMarker>{ "Item", "Created", "Price" },
Size = 50,
Query = new BoolQuery
{
Should = prices.Select(p => new RangeQuery
{
Field = "price",
GreaterThanOrEqualTo = (p * 0.95).ToString(),
LowerThanOrEqualTo = (p * 1.15).ToString()
}.ToContainer())
}.ToContainer()
};
var result = client.Search<Document>(searchRequest);
Console.WriteLine(Encoding.UTF8.GetString(result.RequestInformation.Request));
}
public class Document
{
public string Item { get; set; }
public DateTime Created { get; set;}
public double Price {get; set;}
}
This searches for"document" types in index "index" (strongly typed to your Document POCO), using a bool query and should clauses of ranges constructed from the prices - if a document's price field falls into any one of the ranges, it will be considered a match.
The resulting query DSL from the NEST query above is
{
"size": 50,
"fields": [
"Item",
"Created",
"Price"
],
"query": {
"bool": {
"should": [
{
"range": {
"price": {
"gte": "0.95",
"lte": "1.15"
}
}
},
{
"range": {
"price": {
"gte": "1.9",
"lte": "2.3"
}
}
}
]
}
}
}

ElasticSearch - NEST equivalent for the "nested Filter Query" in Sense?

I am new to Elastic search,
I have object structure like,
class Student
{
int schoolId;
List<Subject> Subjects;
}
class Subject
{
string title;
string type;
}
I am trying to list down all the unique Subject titles. So far, I have the following Query that works in Sense and gives me desired result.
curl -XGET "http://localhost:9200/school-v0.1/student/_search?search_type=count&routing=5" -d '{
"filter":
{
"term": { "schoolId": 5 }
},
"aggs":
{
"SubjectsAggr":
{
"nested": { "path": "Subjects" },
"aggs":
{
"TitlesAggr":
{
"terms": { "field": "subject.title" }
}
}
}
}
}'
But I am not getting the NEST equivalent of the same. Refer below.
Am I missing something?
var result = this.ElasticClient.Search<Student>(q => q
.Index(this.ElasticClient.Index)
.Routing(schoolId)
.SearchType(SearchType.Count)
.Filter(q1 => q1.Term(a => a.schoolId, schoolId))
.Aggregations(student => student.Nested("SubjectsAggr", b => b.Path("subjects")
.Aggregations(sub => sub.Terms("TitlesAggr", s => s.Field("subject.title"))))));
//Below is what I hope to do, but the I get compilation errors because there seems to be something wrong with above NEST query that I have written
var subjectsAggregation = esResult.Aggs.Nested("SubjectsAggr");
var titlesAggregation = subjectsAggregation.Aggs.Terms("TitlesAggr");
var subjects = new List<string>();
foreach (var s in titlesAggregation.Items)
{
subjects.Add(s.Key);
}
Could you please help me find it? Thank you.
My NEST query was indeed correct. I was only not fetching the results out in appropriate way. I am now able to extract the needed information out of the search result as shown below.
var subjectsAggregation = result.Aggs.Nested("SubjectsAggr");
var titlesAggregation = subjectsAggregation.Aggregations["TitlesAggr"];
var titlesBucket = titlesAggregation as Bucket;
var titles = new List<string>();
if (titlesBucket != null)
{
foreach (var title in titlesBucket.Items)
{
titles.Add((((KeyItem) title).Key));
}
}

Resources