Created
May 26, 2020 23:03
-
-
Save JimBobSquarePants/f07c60bd281614bf3f018de8c595c7ee to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Create a query where the result must match or contain the given query | |
// and the subset of properties and filters. | |
descriptor.Query( | |
q => q.Bool( | |
b => b.Must( | |
mu => | |
mu.MultiMatch( | |
m => m.Fields(propertyExpressions) // Search within these properties or all. | |
.Query(searchTerm) // For this query | |
.Operator(Operator.Or))) // In any of the properties. | |
.Filter(filters))); // Further filter results. |
I'm not sure I 100% understand, but I think you want to know how to augment with filters
? One way I've done this in the past with the fluent syntax is to split out the building of query parts into separate methods. For example, in Linqpad with 7.7.0,
private static void Main()
{
var defaultIndex = "products";
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(pool, JsonNetSerializer.Default)
.DefaultIndex(defaultIndex)
// following method calls added only for development, probably don't want these on in production
.DisableDirectStreaming()
.PrettyJson()
.OnRequestCompleted(callDetails =>
{
if (callDetails.RequestBodyInBytes != null)
{
Console.WriteLine(
$"{callDetails.HttpMethod} {callDetails.Uri} \n" +
$"{Encoding.UTF8.GetString(callDetails.RequestBodyInBytes)}");
}
else
{
Console.WriteLine($"{callDetails.HttpMethod} {callDetails.Uri}");
}
Console.WriteLine();
if (callDetails.ResponseBodyInBytes != null)
{
Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
$"{Encoding.UTF8.GetString(callDetails.ResponseBodyInBytes)}\n" +
$"{new string('-', 30)}\n");
}
else
{
Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
$"{new string('-', 30)}\n");
}
});
var client = new ElasticClient(settings);
if (client.Indices.Exists(defaultIndex).Exists)
{
client.Indices.Delete(defaultIndex);
}
client.Indices.Create(defaultIndex, c => c
.Map<Product>(m => m
.AutoMap()
.Properties(p => p
.Object<Category>(o => o
.Name(n => n.Categories)
.Properties(pp => pp
.Keyword(k => k
.Name(n => n.Name)
)
)
)
)
)
);
client.Bulk(b => b
.IndexMany(new [] {
new Product
{
Name = "product1",
Categories = new List<Category>
{
new Category { Name = "category1" },
new Category { Name = "category2" }
}
},
new Product
{
Name = "product2",
Categories = new List<Category>
{
new Category { Name = "category2" },
new Category { Name = "category3" }
}
},
new Product
{
Name = "another product1",
Categories = new List<Category>
{
new Category { Name = "category1" },
new Category { Name = "category2" }
}
},
})
.Refresh(Refresh.WaitFor)
);
var categories = new []
{
"category1", "category2"
};
var propertyExpressions = Nest.Infer.Fields<Product>(p => p.Name);
var searchTerm = "another";
client.Search<Product>(s => s
.Query(q => q
.Bool(b => b
.Must(mu => mu
.MultiMatch(m => m
.Fields(propertyExpressions) // Search within these properties or all.
.Query(searchTerm) // For this query
.Operator(Operator.Or)
)
) // In any of the properties.
.Filter(Filters(categories))
)
)
);
}
public static Func<QueryContainerDescriptor<Product>, QueryContainer>[] Filters(string[] categories)
{
var filters = new List<Func<QueryContainerDescriptor<Product>, QueryContainer>>(categories.Length);
foreach (var c in categories)
{
filters.Add(q => q.Term(p => p.Categories.First().Name, c));
}
return filters.ToArray();
}
public class Product
{
// Id and other properties removed for brevity..
public string Name { get; set; }
public ICollection<Category> Categories { get; set; } = new HashSet<Category>();
}
public class Category
{
// Id and other properties removed for brevity..
public string Name { get; set; }
[Ignore]
public Product Product { get; set; }
}
Another way would be to start with a QueryContainer
and use the overloaded operators on QueryContainer
to combine queries. Using the previous example, the following would build the same query
client.Search<Product>(s => s
.Query(q => BuildQuery(q, searchTerm, propertyExpressions, categories))
);
public static QueryContainer BuildQuery(QueryContainerDescriptor<Product> q, string searchTerm, Fields propertyExpressions, string[] categories)
{
QueryContainer qc = q;
qc &= Query<Product>
.MultiMatch(m => m
.Fields(propertyExpressions) // Search within these properties or all.
.Query(searchTerm) // For this query
.Operator(Operator.Or)
);
foreach (var c in categories)
{
qc &= +Query<Product>
.Term(p => p.Categories.First().Name, c);
}
return qc;
}
which is
{
"query": {
"bool": {
"filter": [{
"term": {
"categories.name": {
"value": "category1"
}
}
}, {
"term": {
"categories.name": {
"value": "category2"
}
}
}],
"must": [{
"multi_match": {
"fields": ["name"],
"operator": "or",
"query": "another"
}
}]
}
}
}
Note that the term query on categories.name
doesn't care if the field is a single value or multiple value. In the case of multiple values, only one value has to match.
The search result in both cases is
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.81427324,
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "guZpU3IBhN_zGyAEwf-g",
"_score" : 0.81427324,
"_source" : {
"name" : "another product1",
"categories" : [
{
"name" : "category1"
},
{
"name" : "category2"
}
]
}
}
]
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Lets' say I have two classes with the following one to many relationship. (Ignore that they should really have many to many)
How would I augment the above query to allow searching by the product name but filtering by a subset of category names? Ideally I should be able to pass something that allows filtering by multiple different properties.