我如何 属性 在 NEST 中汇总 and/or 组合?
How do I property roll up and/or combinations in NEST?
背景
我想做什么
- 我有一份车辆清单。
- 我有一个 API (WebAPI v2),它接收品牌和型号的过滤器列表
- 一个过滤器由 1 个品牌和 0 个或更多型号组成。 (例如 "Honda" 和 ["Civic"、"Accord"])
- 如果过滤器传入品牌但没有型号,我希望它匹配该品牌的所有型号。
- 如果传入带有品牌和型号的过滤器,我希望它只为该品牌制作那些型号。
我正在使用的过滤器对象
public class MakeModelFilter : IMakeModelFilter
{
public string Make { get; set; }
public List<string> Models { get; set; }
}
整个 API 调用的样子
{
"MakeModelFilters": [
{"Make": "BMW", "Models": ["X3", "X5"]}
],
"TypeFilter": [],
"GenericColorFilter": [],
"FeaturesFilter": [],
"MaxMileage" : 100000,
"PriceRange": {"Min": 1, "Max": 1000000},
"SearchText": ""
}
我关心的部分是 MakeAndModelFilters 列表(其余部分按当前设计工作)。
我目前获取搜索结果的方式:
var vehicles = _esClient.Search<Vehicle>(s => s
.From(0).Size(10000)
.Query(q => q
.Filtered(fq => fq
.Filter(ff => ff
.Bool(b => b
.Must(m=> m.And(
m.Or(makeModelFilterList.ToArray()),
m.Or(featureFilters.ToArray()),
m.Or(typeFilters.ToArray()),
priceRangeFilter,
mileageFilter))
)
)
.Query(qq => qq
.QueryString(qs => qs.Query(criteria.SearchText))
)
)
)
);
问题
无论我如何构造过滤器,它似乎都过滤掉了所有文档——这不符合我们的最佳利益。 :) 我的布尔逻辑有问题。
我认为问题出在哪里
我or
一起生成的品牌和型号过滤器列表是通过这种方法生成的:
private List<FilterContainer> GenerateMakeModelFilter(List<MakeModelFilter> makeModelFilters)
{
var filterList = new List<FilterContainer>();
foreach (var filter in makeModelFilters)
{
filterList.Add(GenerateMakeModelFilter(filter));
}
return filterList;
}
此方法调用单独的方法为我拥有的每个 make/model 过滤器生成一个布尔值。
我觉得问题的方法是什么
据我所知,以下方法执行以下操作:
- 如果没有传入make,则抛出异常
- 如果只传入品牌,return 只为该品牌传递一个布尔值。
- 如果传入品牌和型号,return 品牌过滤器的布尔值 + 所有型号项的
or
。例如Make:BMW AND (model:X3 OR model:X5)
代码如下:
private FilterContainer GenerateMakeModelFilter(MakeModelFilter makeModelFilter)
{
if (string.IsNullOrWhiteSpace(makeModelFilter.Make)) { throw new ArgumentNullException(nameof(makeModelFilter));}
var makeFilter = new TermFilter { Field = Property.Path<Vehicle>(it => it.Make), Value = makeModelFilter.Make };
var boolMake = new BoolFilter { Must = new List<FilterContainer> { makeFilter } };
var modelFilters = GenerateFilterList(Property.Path<Vehicle>(it => it.Model), makeModelFilter.Models);
if (!modelFilters.Any())
{
// If it has a make but no model, generate boolFilter make only.
return boolMake;
}
var orModels = new OrFilter {Filters = modelFilters};
var boolModels = new BoolFilter {Must = new List<FilterContainer> {orModels}};
var boolMakeAndModels = new AndFilter {Filters = new List<FilterContainer> {boolMake, boolModels}};
return new BoolFilter {Must = new List<FilterContainer> {boolMakeAndModels}};
}
仅供参考,GenerateFilterList
只是创建了一个术语过滤器列表,return 是该列表。
仅供参考:生成的 ElasticSearch JSON
这可能是我哪里出错的线索(尽管它很大)。我只是盯着它看了太久,我想我看不到它。
{
"from": 0,
"size": 10000,
"query": {
"filtered": {
"filter": {
"bool": {
"must": [
{
"and": {
"filters": [
{
"or": {
"filters": [
{
"bool": {
"must": [
{
"and": {
"filters": [
{
"bool": {
"must": [
{
"term": {
"make": "BMW"
}
}
]
}
},
{
"bool": {
"must": [
{
"or": {
"filters": [
{
"term": {
"model": "x3"
}
},
{
"term": {
"model": "x5"
}
}
]
}
}
]
}
}
]
}
}
]
}
}
]
}
},
{ },
{ },
{
"range": {
"sellingPriceUSD": {
"lte": "1000000",
"gte": "1"
}
}
},
{
"range": {
"miles": {
"lte": "100000"
}
}
}
]
}
}
]
}
}
}
}
}
重构 1:更多地转向按位运算
根据 Martijn 的回答和他引用的 Zachary's post,我已将我的 GenerateFilterList
更新为 return 串联的 filterContainer:
private FilterContainer GenerateFilterList(PropertyPathMarker path, List<string> filter)
{
if (filter == null || filter.Count <= 0){ return null; }
FilterContainer returnFilter = null;
foreach (var aFilter in filter)
{
returnFilter |= new TermFilter {Field = path, Value = aFilter.ToLowerInvariant()};
}
return returnFilter;
}
然后对于我的 GenerateMakeModelFilter
,我对 "model filters" 执行 "and",这应该是按位或基于上面的代码:
private FilterContainer GenerateMakeModelFilter(MakeModelFilter makeModelFilter)
{
if (string.IsNullOrWhiteSpace(makeModelFilter.Make)) { throw new ArgumentNullException(nameof(makeModelFilter)); }
var makeFilter = new TermFilter { Field = Property.Path<Vehicle>(it => it.Make), Value = makeModelFilter.Make };
var modelFilters = GenerateFilterList(Property.Path<Vehicle>(it => it.Model), makeModelFilter.Models);
return makeFilter && modelFilters;
}
这缩短了检索查询的部分:
QueryContainer textQuery = new QueryStringQuery() {Query = criteria.SearchText };
FilterContainer boolFilter = makeModelFilter || featureFilter || typeFilter || priceRangeFilter || mileageFilter;
var vehicles = _esClient.Search<Vehicle>(s => s
.From(0).Size(10000) //TODO: Extract this into a constant or setting in case the inventory grows to 10k+. This prevents it from paging.
.Query(q => q
.Filtered(fq => fq
.Filter(filter => filter.Bool(bf => bf.Must(boolFilter)))
.Query(qq => textQuery)
)
)
);
return vehicles.Documents.ToList<IVehicle>();
...但我仍然没有文件 returned。我到底错过了什么?如果我有一辆型号为 "Civic" 和 "Accord" 的本田汽车,以及一辆没有型号的 "BMW" 汽车,我应该会收到所有带有 honda + civic || 的车辆本田+雅阁||宝马+(任何型号)。我会坚持的。
And,or, & not 过滤器可能无法满足您的要求。它们是一种特殊的过滤器结构,在组合不对位集进行操作的过滤器时性能更好。必须阅读此主题:
https://www.elastic.co/blog/all-about-elasticsearch-filter-bitsets
知道何时使用 and/or/not 过滤器与 bool 过滤器可能会非常混乱,使用 Elasticsearch 2.0,您可以在所有上下文中使用 bool 过滤器,它会知道如何最好地执行 filters/queries其条款。不用你指点了!
此外,尽管 bool filter/query 被命名为 bool,但它执行的是一元 bool,而您可能认为它是二进制 bool。
这就是布尔子句是 must/should/must_not 与 and/or/not 的原因。
在 NEST 中,如果您使用 &&
||
!
运算符并结合括号,我们将组成一个或多个 bool 查询,以便它以您编写的二进制 bool 方式运行在 C# 中。
例如:
.Query(q=>q
(q.Term("language", "php")
&& !q.Term("name", "Elastica")
)
||
q.Term("name", "NEST")
)
如果您需要更动态的列表,您可以使用赋值运算符 !=
和 &=
:
private FilterContainer GenerateMakeModelFilter(List<MakeModelFilter> makeModelFilters)
{
FilterContainer filter = null;
foreach (var filter in makeModelFilters)
{
filter |= GenerateMakeModelFilter(filter);
}
return filter;
}
同样,如果您重构 GenerateMakeModelFilter
以利用 C# 布尔运算符重载,您最终会得到一个更易于阅读和调试的查询。无论是 C# 还是发送到 Elasticsearch 的查询。
我们的文档对此进行了更详细的介绍 http://nest.azurewebsites.net/nest/writing-queries.html
更新
很棒的重构!现在我们可以专注于 elasticsearch 中的映射。当您索引 json 属性 时,它会通过 an analysis chain 获取单个字符串并尝试从中提取 1 个或多个术语,这些术语将存储在 lucene 的倒排索引中。
默认情况下,elasticsearch 将使用 standard analyzer
分析所有字符串字段
在您的情况下,BMW
将通过 standard analyzer 拆分空格(Unicode 标准附件 #29 确切地说 )并将其小写。
因此倒排索引中的术语是bmw
。在 elasticsearch 中,一些查询也会在查询时进行分析,因此 a match query for BMW
在查询倒排索引之前也会被分析并转换为 bmw
,因此无论大小写如何,都会找到文档BMW
在查询时。
您正在使用的术语 query/filter 在查询时 未被 分析,因此它将尝试在倒排索引中查找 BMW
索引只有 bmw
。如果您只想要精确的术语匹配,这很好。如果你设置 your mapping so that a field is not analyzed you could for instance do exact matches on New York
without worrying its actually stored as two separate terms new
and york
and inadvertently also get results from New New York
背景
我想做什么
- 我有一份车辆清单。
- 我有一个 API (WebAPI v2),它接收品牌和型号的过滤器列表
- 一个过滤器由 1 个品牌和 0 个或更多型号组成。 (例如 "Honda" 和 ["Civic"、"Accord"])
- 如果过滤器传入品牌但没有型号,我希望它匹配该品牌的所有型号。
- 如果传入带有品牌和型号的过滤器,我希望它只为该品牌制作那些型号。
我正在使用的过滤器对象
public class MakeModelFilter : IMakeModelFilter
{
public string Make { get; set; }
public List<string> Models { get; set; }
}
整个 API 调用的样子
{
"MakeModelFilters": [
{"Make": "BMW", "Models": ["X3", "X5"]}
],
"TypeFilter": [],
"GenericColorFilter": [],
"FeaturesFilter": [],
"MaxMileage" : 100000,
"PriceRange": {"Min": 1, "Max": 1000000},
"SearchText": ""
}
我关心的部分是 MakeAndModelFilters 列表(其余部分按当前设计工作)。
我目前获取搜索结果的方式:
var vehicles = _esClient.Search<Vehicle>(s => s
.From(0).Size(10000)
.Query(q => q
.Filtered(fq => fq
.Filter(ff => ff
.Bool(b => b
.Must(m=> m.And(
m.Or(makeModelFilterList.ToArray()),
m.Or(featureFilters.ToArray()),
m.Or(typeFilters.ToArray()),
priceRangeFilter,
mileageFilter))
)
)
.Query(qq => qq
.QueryString(qs => qs.Query(criteria.SearchText))
)
)
)
);
问题
无论我如何构造过滤器,它似乎都过滤掉了所有文档——这不符合我们的最佳利益。 :) 我的布尔逻辑有问题。
我认为问题出在哪里
我or
一起生成的品牌和型号过滤器列表是通过这种方法生成的:
private List<FilterContainer> GenerateMakeModelFilter(List<MakeModelFilter> makeModelFilters)
{
var filterList = new List<FilterContainer>();
foreach (var filter in makeModelFilters)
{
filterList.Add(GenerateMakeModelFilter(filter));
}
return filterList;
}
此方法调用单独的方法为我拥有的每个 make/model 过滤器生成一个布尔值。
我觉得问题的方法是什么
据我所知,以下方法执行以下操作:
- 如果没有传入make,则抛出异常
- 如果只传入品牌,return 只为该品牌传递一个布尔值。
- 如果传入品牌和型号,return 品牌过滤器的布尔值 + 所有型号项的
or
。例如Make:BMW AND (model:X3 OR model:X5)
代码如下:
private FilterContainer GenerateMakeModelFilter(MakeModelFilter makeModelFilter)
{
if (string.IsNullOrWhiteSpace(makeModelFilter.Make)) { throw new ArgumentNullException(nameof(makeModelFilter));}
var makeFilter = new TermFilter { Field = Property.Path<Vehicle>(it => it.Make), Value = makeModelFilter.Make };
var boolMake = new BoolFilter { Must = new List<FilterContainer> { makeFilter } };
var modelFilters = GenerateFilterList(Property.Path<Vehicle>(it => it.Model), makeModelFilter.Models);
if (!modelFilters.Any())
{
// If it has a make but no model, generate boolFilter make only.
return boolMake;
}
var orModels = new OrFilter {Filters = modelFilters};
var boolModels = new BoolFilter {Must = new List<FilterContainer> {orModels}};
var boolMakeAndModels = new AndFilter {Filters = new List<FilterContainer> {boolMake, boolModels}};
return new BoolFilter {Must = new List<FilterContainer> {boolMakeAndModels}};
}
仅供参考,GenerateFilterList
只是创建了一个术语过滤器列表,return 是该列表。
仅供参考:生成的 ElasticSearch JSON
这可能是我哪里出错的线索(尽管它很大)。我只是盯着它看了太久,我想我看不到它。
{
"from": 0,
"size": 10000,
"query": {
"filtered": {
"filter": {
"bool": {
"must": [
{
"and": {
"filters": [
{
"or": {
"filters": [
{
"bool": {
"must": [
{
"and": {
"filters": [
{
"bool": {
"must": [
{
"term": {
"make": "BMW"
}
}
]
}
},
{
"bool": {
"must": [
{
"or": {
"filters": [
{
"term": {
"model": "x3"
}
},
{
"term": {
"model": "x5"
}
}
]
}
}
]
}
}
]
}
}
]
}
}
]
}
},
{ },
{ },
{
"range": {
"sellingPriceUSD": {
"lte": "1000000",
"gte": "1"
}
}
},
{
"range": {
"miles": {
"lte": "100000"
}
}
}
]
}
}
]
}
}
}
}
}
重构 1:更多地转向按位运算
根据 Martijn 的回答和他引用的 Zachary's post,我已将我的 GenerateFilterList
更新为 return 串联的 filterContainer:
private FilterContainer GenerateFilterList(PropertyPathMarker path, List<string> filter)
{
if (filter == null || filter.Count <= 0){ return null; }
FilterContainer returnFilter = null;
foreach (var aFilter in filter)
{
returnFilter |= new TermFilter {Field = path, Value = aFilter.ToLowerInvariant()};
}
return returnFilter;
}
然后对于我的 GenerateMakeModelFilter
,我对 "model filters" 执行 "and",这应该是按位或基于上面的代码:
private FilterContainer GenerateMakeModelFilter(MakeModelFilter makeModelFilter)
{
if (string.IsNullOrWhiteSpace(makeModelFilter.Make)) { throw new ArgumentNullException(nameof(makeModelFilter)); }
var makeFilter = new TermFilter { Field = Property.Path<Vehicle>(it => it.Make), Value = makeModelFilter.Make };
var modelFilters = GenerateFilterList(Property.Path<Vehicle>(it => it.Model), makeModelFilter.Models);
return makeFilter && modelFilters;
}
这缩短了检索查询的部分:
QueryContainer textQuery = new QueryStringQuery() {Query = criteria.SearchText };
FilterContainer boolFilter = makeModelFilter || featureFilter || typeFilter || priceRangeFilter || mileageFilter;
var vehicles = _esClient.Search<Vehicle>(s => s
.From(0).Size(10000) //TODO: Extract this into a constant or setting in case the inventory grows to 10k+. This prevents it from paging.
.Query(q => q
.Filtered(fq => fq
.Filter(filter => filter.Bool(bf => bf.Must(boolFilter)))
.Query(qq => textQuery)
)
)
);
return vehicles.Documents.ToList<IVehicle>();
...但我仍然没有文件 returned。我到底错过了什么?如果我有一辆型号为 "Civic" 和 "Accord" 的本田汽车,以及一辆没有型号的 "BMW" 汽车,我应该会收到所有带有 honda + civic || 的车辆本田+雅阁||宝马+(任何型号)。我会坚持的。
And,or, & not 过滤器可能无法满足您的要求。它们是一种特殊的过滤器结构,在组合不对位集进行操作的过滤器时性能更好。必须阅读此主题:
https://www.elastic.co/blog/all-about-elasticsearch-filter-bitsets
知道何时使用 and/or/not 过滤器与 bool 过滤器可能会非常混乱,使用 Elasticsearch 2.0,您可以在所有上下文中使用 bool 过滤器,它会知道如何最好地执行 filters/queries其条款。不用你指点了!
此外,尽管 bool filter/query 被命名为 bool,但它执行的是一元 bool,而您可能认为它是二进制 bool。
这就是布尔子句是 must/should/must_not 与 and/or/not 的原因。
在 NEST 中,如果您使用 &&
||
!
运算符并结合括号,我们将组成一个或多个 bool 查询,以便它以您编写的二进制 bool 方式运行在 C# 中。
例如:
.Query(q=>q
(q.Term("language", "php")
&& !q.Term("name", "Elastica")
)
||
q.Term("name", "NEST")
)
如果您需要更动态的列表,您可以使用赋值运算符 !=
和 &=
:
private FilterContainer GenerateMakeModelFilter(List<MakeModelFilter> makeModelFilters)
{
FilterContainer filter = null;
foreach (var filter in makeModelFilters)
{
filter |= GenerateMakeModelFilter(filter);
}
return filter;
}
同样,如果您重构 GenerateMakeModelFilter
以利用 C# 布尔运算符重载,您最终会得到一个更易于阅读和调试的查询。无论是 C# 还是发送到 Elasticsearch 的查询。
我们的文档对此进行了更详细的介绍 http://nest.azurewebsites.net/nest/writing-queries.html
更新
很棒的重构!现在我们可以专注于 elasticsearch 中的映射。当您索引 json 属性 时,它会通过 an analysis chain 获取单个字符串并尝试从中提取 1 个或多个术语,这些术语将存储在 lucene 的倒排索引中。
默认情况下,elasticsearch 将使用 standard analyzer
分析所有字符串字段在您的情况下,BMW
将通过 standard analyzer 拆分空格(Unicode 标准附件 #29 确切地说 )并将其小写。
因此倒排索引中的术语是bmw
。在 elasticsearch 中,一些查询也会在查询时进行分析,因此 a match query for BMW
在查询倒排索引之前也会被分析并转换为 bmw
,因此无论大小写如何,都会找到文档BMW
在查询时。
您正在使用的术语 query/filter 在查询时 未被 分析,因此它将尝试在倒排索引中查找 BMW
索引只有 bmw
。如果您只想要精确的术语匹配,这很好。如果你设置 your mapping so that a field is not analyzed you could for instance do exact matches on New York
without worrying its actually stored as two separate terms new
and york
and inadvertently also get results from New New York