在 elasticsearch filter 中实现 Array.Except(Array2) > 0 查询?



假设我有以下文档索引:

[
{
"Id": 1,
"Numbers": [1, 2, 3]
},
{
"Id": 2,
"Numbers": [4, 5]
}    
]

我有一个参数[1,2,4,5],它定义了我不允许看到的数字-我想找到"numbers"数组中至少有一个元素not在我的输入数组中的文档(所以在这种情况下,应该返回第一个文档(。

真实场景是查找不包含属于特定产品类型的产品的组(或谁的子组(。我已经递归地索引了产品类型ID(在示例中用数字表示(,我想找到包含不属于我的输入参数的产品的组(我的输入变量是我不允许看到的产品类型ID的数组(

我应该使用哪个查询/过滤器,以及应该如何构建它?我考虑了以下内容:

return desc.Bool(b => b
.MustNot(mn => mn.Bool(mnb => mnb.Must(mnbm => mnbm.Terms(t => t.ItemGroups, permissions.RestrictedItemGroups) && mnbm.Term(t => t.ItemGroupCount, permissions.RestrictedItemGroups.Count())))));

但问题是,如果我有6个受限项目组,其中给定的组包含3个受限组,那么我找不到任何匹配项,因为计数不匹配。现在这很有道理。作为一种变通方法,我在C#中实现了Results.Except(Restricted(,以在搜索后过滤出受限制的组,但我很想在弹性搜索中实现它。

新答案

我将在下面留下较老的答案,因为它可能对其他人有用。在您的情况下,您希望筛选出不匹配的文档,而不仅仅是标记它们。因此,下面的查询将得到您所期望的,即只有第一个文档:

POST test/_search
{
"query": {
"script": {
"script": {
"source": """
// copy the doc values into a temporary list
def tmp = new ArrayList(doc.Numbers.values);
// remove all ids from the params
tmp.removeIf(n -> params.ids.contains((int)n));
// return true if the array still contains ids, false if not
return tmp.size() > 0;
""",
"params": {
"ids": [
1,
2,
4,
5
]
}
}
}
}
}

旧答案

解决这个问题的一种方法是使用脚本字段,该字段将根据您的条件返回true或false:

POST test/_search
{
"_source": true,
"script_fields": {
"not_present": {
"script": {
"source": """
// copy the numbers array
def tmp = params._source.Numbers;
// remove all ids from the params
tmp.removeIf(n -> params.ids.contains(n));
// return true if the array still contains data, false if not
return tmp.length > 0;
""",
"params": {
"ids": [ 1, 2, 4, 5 ]
}
}
}
}
}

结果如下:

"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "test",
"_type" : "doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"Id" : 2,
"Numbers" : [
4,
5
]
},
"fields" : {
"not_present" : [
false                           <--- you don't want this doc
]
}
},
{
"_index" : "test",
"_type" : "doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"Id" : 1,
"Numbers" : [
1,
2,
3
]
},
"fields" : {
"not_present" : [
true                            <--- you want this one, though
]
}
}
]
}
}

terms_set查询似乎非常适合此项;它类似于terms查询,但有一个额外的区别,即您可以指定有多少术语必须与从输入或每个文档派生的动态值相匹配。

在您的情况下,您希望获得文档的倒数,其中Numbers数组中的所有数字都在输入项中,即如果Numbers数组至少包含一个不在输入项内的值,则应将其视为匹配。

以下内容将适用于

private static void Main()
{
var defaultIndex = "my_index";
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(pool)
.DefaultIndex(defaultIndex)
.DefaultFieldNameInferrer(f => f);
var client = new ElasticClient(settings);
if (client.IndexExists(defaultIndex).Exists)
client.DeleteIndex(defaultIndex);
var createIndexResponse = client.CreateIndex(defaultIndex, c => c
.Settings(s => s
.NumberOfShards(1)
.NumberOfReplicas(0)
)
.Mappings(m => m
.Map<MyDocument>(mm => mm.AutoMap())
)
);
var bulkResponse = client.Bulk(b => b
.IndexMany(new []
{
new MyDocument { Id = 1, Numbers = new int[] { 1, 2, 3 }},
new MyDocument { Id = 2, Numbers = new int[] { 4, 5 }},
new MyDocument { Id = 3, Numbers = new int[] { }},
})
.Refresh(Refresh.WaitFor)
);
var searchResponse = client.Search<MyDocument>(s => s
.Query(q => (!q
.TermsSet(ts => ts
.Field(f => f.Numbers)
.Terms(1, 2, 4, 5)
.MinimumShouldMatchScript(sc => sc
.Source("doc['Numbers'].size()")
)
)) && q
.Exists(ex => ex
.Field(f => f.Numbers)
)
)
);
}
public class MyDocument 
{
public int Id { get; set; }
public int[] Numbers { get; set; }
}

生成的搜索请求看起来像

{
"query": {
"bool": {
"must": [
{
"exists": {
"field": "Numbers"
}
}
],
"must_not": [
{
"terms_set": {
"Numbers": {
"minimum_should_match_script": {
"source": "doc['Numbers'].size()"
},
"terms": [
1,
2,
4,
5
]
}
}
}
]
}
}
}

结果是

{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "my_index",
"_type" : "mydocument",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"Id" : 1,
"Numbers" : [
1,
2,
3
]
}
}
]
}
}

terms_set查询在must_not子句中,用于反转Numbers中的所有值都在术语输入中的匹配,并与Numbers上的exists查询组合,以排除没有Numbers值的文档,如Id为3的示例文档中所示。

通过在文档中的另一个字段中索引Numbers数组的长度,然后使用MinimumShouldMatchField(...)而不是脚本,可以使其性能更好。只需要确保这两个属性保持同步,这在带有返回Numbers数组长度值的属性getter的C#POCO中很容易做到。

最新更新