当前位置: 首页 > 知识库问答 >
问题:

是否可以跨父文档和嵌套文档使用多匹配查询?

杜嘉木
2023-03-14

假设模型:

json prettyprint-override">{
  "group" : "fans",
  "name": "Anne",
  "user" : [
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]
}

由于用户是“嵌套”类型,我想使用多匹配查询来选择在父级和内部层匹配的匹配文档(和内部命中)。

搜索粉丝安妮应该给我上面的文档以及所有内部命中,因为它在父级别上完全匹配。

搜索John Smith应该会给我上面的文档,但只与第一个内部命中匹配,因为它在父级上不匹配,也与第二个内部命中不匹配。

搜索<code>粉丝Smith</code>应该会给我上面的文档,但只有第一个内部命中,因为组合结果与父字段和第一个内部击中字段相匹配。它不应该返回第二个内部命中,因为它自己和父字段中都缺少Smith

使用boolquery很容易解决用例1和用例2,bool query将父级的多匹配查询和嵌套查询中的另一个多匹配查询连接在一起(下面是Java代码):

boolQuery()
    .should(multiMatchQuery(searchTerm).operator(AND).type(CROSS_FIELDS))
    .should(nestedQuery("user", multiMatchQuery(searchTerm).operator(AND).type(CROSS_FIELDS), NONE))

这是我坚持的第三个用例。上面的查询只适用于父级或嵌套级,但不能组合使用。我曾尝试将“include_in_parent”添加到嵌套类型中,以便将其与父类型一起索引,但随后它会在搜索中匹配,如JohnAlice,这是我不希望的。

共有2个答案

霍襦宗
2023-03-14

我做了最后的努力,试图手动绕过这个问题,并意外地找到了一个方法。这很复杂,但这就是我目前解决的方法。(我将在代码片段下面解释它是如何工作的。)

private static final String FULL_MATCH_ON_PARENT = "full-match-on-parent";
private static final String PARTIAL_MATCH_ON_PARENT = "partial-match-on-parent";
private static final String PARTIAL_MATCH_ON_NESTED = "partial-match-on-nested";
private final RestHighLevelClient client;
private final ObjectMapper objectMapper;

public List<ParentObject> search(String searchTerm) throws IOException {
    SearchSourceBuilder searchSourceBuilder = searchSource().size(20).query(queryForMatchingParents(searchTerm));
    SearchRequest request = new SearchRequest().indices("my-index").source(searchSourceBuilder);
    SearchResponse search = client.search(request, DEFAULT);
    return Stream.of(search.getHits().getHits())
            .map(hit -> {
                ParentObject parent = readJson(hit.getSourceAsString(), ParentObject.class);
                if (!Arrays.asList(hit.getMatchedQueries()).contains(FULL_MATCH_ON_PARENT)) {
                    List<String> nestedToKeep = Arrays.stream(hit.getMatchedQueries())
                            .filter(queryName -> queryName.startsWith(PARTIAL_MATCH_ON_PARENT))
                            .map(partialMatchOnParentQueryName -> partialMatchOnParentQueryName.replace("parent", "nested"))
                            .flatMap(partialMatchOnNestedQueryName -> Arrays.stream(hit.getInnerHits().get(partialMatchOnNestedQueryName).getHits()))
                            .map(innerHit -> readJson(innerHit.getSourceAsString(), NestedObject.class).getId())
                            .distinct()
                            .collect(toList());
                    parent.getNestedObjects().removeAll(parent.getNestedObjects().stream()
                            .filter(nested -> !nestedToKeep.contains(nested.getId()))
                            .collect(toList()));
                }
                return parent;
            })
            .filter(Objects::nonNull)
            .collect(toList());
}

private QueryBuilder queryForMatchingParents(String searchTerm) {
    BoolQueryBuilder superAggregateQuery = boolQuery();
    MultiMatchQueryBuilder matchParentQuery = multiMatchQuery(searchTerm)
            .operator(Operator.AND)
            .type(MultiMatchQueryBuilder.Type.CROSS_FIELDS).queryName(FULL_MATCH_ON_PARENT);
    superAggregateQuery.should(matchParentQuery);
    BoolQueryBuilder aggregateQuery = boolQuery();
    aggregateQuery.mustNot(matchParentQuery);
    BoolQueryBuilder matchNestedQuery = boolQuery();
    int counter = 1;
    for (Pair<String, String> searchTermPair : getParentNestedPairsOfSearchTerm(searchTerm)) {
        matchNestedQuery.should(queryPartialInnerHit(searchTermPair.getLeft(), searchTermPair.getRight(), counter));
        counter++;
    }
    aggregateQuery.must(matchNestedQuery);
    superAggregateQuery.should(aggregateQuery);
    return superAggregateQuery;
}

private QueryBuilder queryPartialInnerHit(String parentSearchTerm, String nestedSearchTerm, int counter) {
    BoolQueryBuilder splitBoolQuery = boolQuery();
    if (StringUtils.isNotEmpty(parentSearchTerm)) {
        splitBoolQuery.must(multiMatchQuery(parentSearchTerm)
                .operator(Operator.AND)
                .type(MultiMatchQueryBuilder.Type.CROSS_FIELDS)).queryName(PARTIAL_MATCH_ON_PARENT + "-" + counter);
    } else {
        // this is necessary because we still need the queryname to trigger for empty string in parentSearchTerm
        splitBoolQuery.must(matchAllQuery()).queryName(PARTIAL_MATCH_ON_PARENT + "-" + counter);
    }
    splitBoolQuery.must(nestedQuery("nested",
            multiMatchQuery(nestedSearchTerm)
                    .operator(Operator.AND)
                    .fuzzyTranspositions(false)
                    .type(MultiMatchQueryBuilder.Type.CROSS_FIELDS),
            ScoreMode.Min).innerHit(new InnerHitBuilder(PARTIAL_MATCH_ON_NESTED + "-" + counter).setExplain(true)));
    return splitBoolQuery;
}

private List<Pair<String, String>> getParentNestedPairsOfSearchTerm(String searchTerm) {
    Set<String> words = new HashSet<>(Arrays.asList(searchTerm.split(" ")));
    Set<Set<String>> powerSet = powerSet(words);
    powerSet = powerSet.stream().filter(set -> set.size() < words.size()).collect(Collectors.toSet());

    return powerSet.stream()
            .map(set -> {
                ArrayList<String> truncatedWords = new ArrayList<>(words);
                truncatedWords.removeAll(set);
                String words1 = String.join(" ", set);
                String words2 = String.join(" ", truncatedWords);
                return new ImmutablePair<>(words1, words2);
            })
            .collect(Collectors.toList());
}

private <T> Set<Set<T>> powerSet(Set<T> originalSet) {
    Set<Set<T>> sets = new HashSet<>();
    if (originalSet.isEmpty()) {
        sets.add(new HashSet<>());
        return sets;
    }
    List<T> list = new ArrayList<>(originalSet);
    T head = list.get(0);
    Set<T> rest = new HashSet<>(list.subList(1, list.size()));
    for (Set<T> set : powerSet(rest)) {
        Set<T> newSet = new HashSet<>();
        newSet.add(head);
        newSet.addAll(set);
        sets.add(newSet);
        sets.add(set);
    }
    return sets;
}

private <T> T readJson(String json, Class<T> objectClass) throws IOException {
    return objectMapper.readValue(json, objectClass);
}

由于我在问题中解释的原因,Elasticsearch不能同时在父对象和嵌套对象中搜索给定的查询。因此,我创建了一个幂集来查找所有要在父对象和嵌套对象中搜索的组合。例如,如果我输入3个搜索词(< code >一二三),我将查找2^3=8组合。在父字段中搜索< code >一,在嵌套字段中搜索< code >二三,依此类推。

我为每个部分匹配(在父级和嵌套级)分配了一个命名查询,具有相同的数字后缀“计数器”,以识别哪些属于一起。当我们得到搜索响应时,这变得很重要。

在我谈论我们如何解释结果之前,我应该提一下,对于部分匹配,只有2^3-1组合。我认为parent上的完全匹配是一个特例,因为在这种情况下,我不需要过滤任何返回的嵌套对象。因此,它具有不同的命名查询< code > FULL _ MATCH _ ON _ PARENT 。

在响应中,我们为每个命中提取的命名查询,以在父级别上完全匹配或部分匹配。如果不存在完全匹配(基于缺少FULL_MATCH_ON_PARENT匹配的查询),则评估嵌套对象以进行丢弃。只应保留具有匹配的部分父命中的嵌套对象,因此应循环访问当前具有父项上的部分匹配{number} 命名查询,并检索具有嵌套对象的相应内部点击。从那里开始,它应该从代码中不言自明。

广泛的集成测试都已通过此解决方案。

公孙新觉
2023-03-14

您无法在多标记查询中处理嵌套字段和非嵌套字段。由于嵌套文档的性质。

因此,我认为唯一的解决方案是更改模型并复制每个嵌套文档中的名称字段。因此,您的请求逻辑是加入父级上的多匹配查询,以及针对在组/名称/第一个/最后一个字段中搜索的粉丝的嵌套查询。

我知道你当然不想改变模型,但在使用ElasticSearch时,你必须调整模型以匹配你想要提供的搜索功能。而不是相反;)

 类似资料:
  • 我是Elasticsearch的新手,我提出了一个问题,Elasticsearch嵌套查询是否只能为嵌套字段返回匹配的嵌套文档。 对于示例,我有一个名为的类型,其中嵌套字段名为 和嵌套查询 我需要的是搜索有提到足球的评论的博客文章,每个博客文章的评论数与足球相匹配(在例子中它数为1,因为另一个评论刚刚提到篮球)。 然而,Elasticsearch似乎总是返回完整的文档,所以我如何才能实现它,或者我

  • 给定一个场景,其中我有一个根文档和一个对象数组。是否可以返回包含一组对象的文档? 例如: 文件1 文件2 我需要一个查询,该查询将根据以下内容匹配并返回Doc1: 在数组中包含key1=value1的对象的文档 这可能吗?

  • 我是elasticsearch的新手,对如何进行过滤器、查询和聚合有一些想法,但不确定如何解决下面的问题。我希望能够从下面显示的文档中只查询公司的最新交付(日期和crate_quantity)。我不确定如何去做。有没有办法使用最大聚合从每个文档中只提取最近的交付?

  • 问题内容: 我正在为我们运行概念验证,以便对ES中更多“标准化”的数据运行嵌套查询。 例如带有嵌套 客户->-名称 -电子邮件-事件->-创建-类型 现在,我可以将给定客户的事件列表移至另一位客户。例如,客户A有50个事件客户B有5000个事件 我现在想将所有事件从客户A移动到客户B 拥有数百万客户的规模,并且在UI中针对图形进行查询,Parent / Child更适合还是应该能够嵌套处理? 在我

  • 尝试排除其中一个子文档与查询不匹配的顶级文档。 对于下面的示例,我试图排除其中一个嵌套作业具有并且与匹配的所有文档。但是,由于其中一个嵌套作业文档与和公司匹配,因此返回此文档。我使用的是一个嵌套查询,其中公司名称必须匹配,并且过滤器的当前值为false。我如何才能使以下文件不被退回?

  • 问题内容: Elasticsearch具有嵌套文档(出色)。我想用它来存储消息(顶级文档)及其作者(嵌套文档)。 由于一个作者可以拥有许多条消息-可以将一个版本的Author引用为多条消息的子级吗? 这样,如果您在一个地方更新Author数据,它将在所有引用它们的地方更新。 注意:这与以下内容有关:如何在Elasticsearch中或在Lucene级别进行联接-此处的答案也可以解决该问题。 问题答