es 从某个字段中的相同数据取任意一个数据

本文介绍了Elasticsearch中字段折叠的概念及其应用场景。字段折叠能够帮助搜索结果更具多样性,避免重复内容过多显示。通过实例展示了如何配置字段折叠及inner_hits参数以优化搜索结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

So,什么是字段折叠,可以理解就是按特定字段进行合并去重,比如我们有一个菜谱搜索,我希望按菜谱的“菜系”字段进行折叠,即返回结果每个菜系都返回一个结果,也就是按菜系去重,我搜索关键字“鱼”,要去返回的结果里面各种菜系都有,有湘菜,有粤菜,有中餐,有西餐,别全是湘菜,就是这个意思,通过按特定字段折叠之后,来丰富搜索结果的多样性。
 
说到这里,有人肯定会想到,使用 term agg+ top hits agg 来实现啊,这种组合两种聚和的方式可以实现上面的功能,不过也有一些局限性,比如,不能分页, #4915 ;结果不够精确(top term+top hits,es 的聚合实现选择了牺牲精度来提高速度);数据量大的情况下,聚合比较慢,影响搜索体验。
 
而新的的字段折叠的方式是怎么实现的的呢,有这些要点:
  1. 折叠+取 inner_hits 分两阶段执行(组合聚合的方式只有一个阶段),所以 top hits 永远是精确的。
  2. 字段折叠只在 top hits 层执行,不需要每次都在完整的结果集上对为每个折叠主键计算实际的 doc values 值,只对 top hits 这小部分数据操作就可以,和 term agg 相比要节省很多内存。
  3. 因为只在 top hits 上进行折叠,所以相比组合聚合的方式,速度要快很多。
  4. 折叠 top docs 不需要使用全局序列(global ordinals)来转换 string,相比 agg 这也节省了很多内存。
  5. 分页成为可能,和常规搜索一样,具有相同的局限,先获取 from+size 的内容,再合并。
  6. search_after 和 scroll 暂未实现,不过具备可行性。
  7.  折叠只影响搜索结果,不影响聚合,搜索结果的 total 是所有的命中纪录数,去重的结果数未知(无法计算)。
 
下面来看看具体的例子,就知道怎么回事了,使用起来很简单。
  • 先准备索引和数据,这里以菜谱为例,name:菜谱名,type 为菜系,rating 为用户的累积平均评分
DELETE recipes
PUT recipes
POST recipes/type/_mapping
{
  "properties": {
    "name":{
      "type": "text"
    },
    "rating":{
      "type": "float"
    },"type":{
      "type": "keyword"
    }
  }
}
POST recipes/type/
{
  "name":"清蒸鱼头","rating":1,"type":"湘菜"
}

POST recipes/type/
{
  "name":"剁椒鱼头","rating":2,"type":"湘菜"
}

POST recipes/type/
{
  "name":"红烧鲫鱼","rating":3,"type":"湘菜"
}

POST recipes/type/
{
  "name":"鲫鱼汤(辣)","rating":3,"type":"湘菜"
}

POST recipes/type/
{
  "name":"鲫鱼汤(微辣)","rating":4,"type":"湘菜"
}

POST recipes/type/
{
  "name":"鲫鱼汤(变态辣)","rating":5,"type":"湘菜"
}

POST recipes/type/
{
  "name":"广式鲫鱼汤","rating":5,"type":"粤菜"
}

POST recipes/type/
{
  "name":"鱼香肉丝","rating":2,"type":"川菜"
}

POST recipes/type/
{
  "name":"奶油鲍鱼汤","rating":2,"type":"西菜"

  • 现在我们看看普通的查询效果是怎么样的,搜索关键字带“鱼”的菜,返回3条数据
POST recipes/type/_search
{
  "query": {"match": {
    "name": "鱼"
  }},"size": 3
全是湘菜,我的天,最近上火不想吃辣,这个第一页的结果对我来说就是垃圾,如下:
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": 0.26742277,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYF_OA-dG63Txsd",
        "_score": 0.26742277,
        "_source": {
          "name": "鲫鱼汤(变态辣)",
          "rating": 5,
          "type": "湘菜"
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHXO_OA-dG63Txsa",
        "_score": 0.19100356,
        "_source": {
          "name": "红烧鲫鱼",
          "rating": 3,
          "type": "湘菜"
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHWy_OA-dG63TxsZ",
        "_score": 0.19100356,
        "_source": {
          "name": "剁椒鱼头",
          "rating": 2,
          "type": "湘菜"
        }
      }
    ]
  }
}
我们再看看,这次我想加个评分排序,大家都喜欢的是那些,看看有没有喜欢吃的,执行查询:
POST recipes/type/_search
{
  "query": {"match": {
    "name": "鱼"
  }},"sort": [
    {
      "rating": {
        "order": "desc"
      }
    }
  ],"size": 3
结果稍微好点了,不过3个里面2个是湘菜,还是有点不合适,结果如下:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYF_OA-dG63Txsd",
        "_score": null,
        "_source": {
          "name": "鲫鱼汤(变态辣)",
          "rating": 5,
          "type": "湘菜"
        },
        "sort": [
          5
        ]
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYW_OA-dG63Txse",
        "_score": null,
        "_source": {
          "name": "广式鲫鱼汤",
          "rating": 5,
          "type": "粤菜"
        },
        "sort": [
          5
        ]
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHX7_OA-dG63Txsc",
        "_score": null,
        "_source": {
          "name": "鲫鱼汤(微辣)",
          "rating": 4,
          "type": "湘菜"
        },
        "sort": [
          4
        ]
      }
    ]
  }
}
现在我知道了,我要看看其他菜系,这家不是还有西餐、广东菜等各种菜系的么,来来,帮我每个菜系来一个菜看看,换 terms agg 先得到唯一的 term 的 bucket,再组合 top_hits agg,返回按评分排序的第一个 top hits,有点复杂,没关系,看下面的查询就知道了:
GET recipes/type/_search
{
  "query": {
    "match": {
      "name": "鱼"
    }
  },
  "sort": [
    {
      "rating": {
        "order": "desc"
      }
    }
  ],"aggs": {
    "type": {
      "terms": {
        "field": "type",
        "size": 10
      },"aggs": {
        "rated": {
          "top_hits": {
            "sort": [{
              "rating": {"order": "desc"}
            }], 
            "size": 1
          }
        }
      }
    }
  }, 
  "size": 0,
  "from": 0
看下面的结果,虽然 json 结构有点复杂,不过总算是我们想要的结果了,湘菜、粤菜、川菜、西菜都出来了,每样一个,不重样:
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "type": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "湘菜",
          "doc_count": 6,
          "rated": {
            "hits": {
              "total": 6,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYF_OA-dG63Txsd",
                  "_score": null,
                  "_source": {
                    "name": "鲫鱼汤(变态辣)",
                    "rating": 5,
                    "type": "湘菜"
                  },
                  "sort": [
                    5
                  ]
                }
              ]
            }
          }
        },
        {
          "key": "川菜",
          "doc_count": 1,
          "rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYr_OA-dG63Txsf",
                  "_score": null,
                  "_source": {
                    "name": "鱼香肉丝",
                    "rating": 2,
                    "type": "川菜"
                  },
                  "sort": [
                    2
                  ]
                }
              ]
            }
          }
        },
        {
          "key": "粤菜",
          "doc_count": 1,
          "rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYW_OA-dG63Txse",
                  "_score": null,
                  "_source": {
                    "name": "广式鲫鱼汤",
                    "rating": 5,
                    "type": "粤菜"
                  },
                  "sort": [
                    5
                  ]
                }
              ]
            }
          }
        },
        {
          "key": "西菜",
          "doc_count": 1,
          "rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHY3_OA-dG63Txsg",
                  "_score": null,
                  "_source": {
                    "name": "奶油鲍鱼汤",
                    "rating": 2,
                    "type": "西菜"
                  },
                  "sort": [
                    2
                  ]
                }
              ]
            }
          }
        }
      ]
    }
  }
}
上面的实现方法,前面已经说了,可以做,有局限性,那看看新的字段折叠法如何做到呢,查询如下,加一个 collapse 参数,指定对那个字段去重就行了,这里当然对菜系“type”字段进行去重了:
GET recipes/type/_search
{
  "query": {
    "match": {
      "name": "鱼"
    }
  },
  "collapse": {
    "field": "type"
  },
  "size": 3,
  "from": 0
}
结果很理想嘛,命中结果还是熟悉的那个味道(和查询结果长的一样嘛),如下:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRJ_OA-dG63TxpW",
        "_score": 0.018980097,
        "_source": {
          "name": "鲫鱼汤(微辣)",
          "rating": 4,
          "type": "湘菜"
        },
        "fields": {
          "type": [
            "湘菜"
          ]
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRk_OA-dG63TxpZ",
        "_score": 0.013813315,
        "_source": {
          "name": "鱼香肉丝",
          "rating": 2,
          "type": "川菜"
        },
        "fields": {
          "type": [
            "川菜"
          ]
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRb_OA-dG63TxpY",
        "_score": 0.0125863515,
        "_source": {
          "name": "广式鲫鱼汤",
          "rating": 5,
          "type": "粤菜"
        },
        "fields": {
          "type": [
            "粤菜"
          ]
        }
      }
    ]
  }
}
我再试试翻页,把 from 改一下,现在返回了3条数据,from 改成3,新的查询如下:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoDNlRw_OA-dG63Txpa",
        "_score": 0.012546891,
        "_source": {
          "name": "奶油鲍鱼汤",
          "rating": 2,
          "type": "西菜"
        },
        "fields": {
          "type": [
            "西菜"
          ]
        }
      }
    ]
  }
}
上面的结果只有一条了,去重之后本来就只有4条数据,上面的工作正常,每个菜系只有一个菜啊,那我不乐意了,帮我每个菜系里面多返回几条,我好选菜啊,加上参数 inner_hits 来控制返回的条数,这里返回2条,按 rating 也排个序,新的查询构造如下:
GET recipes/type/_search
{
  "query": {
    "match": {
      "name": "鱼"
    }
  },
  "collapse": {
    "field": "type",
    "inner_hits": {
      "name": "top_rated",
      "size": 2,
      "sort": [
        {
          "rating": "desc"
        }
      ]
    }
  },
  "sort": [
    {
      "rating": {
        "order": "desc"
      }
    }
  ],
  "size": 2,
  "from": 0
}
查询结果如下,完美:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 9,
    "max_score": null,
    "hits": [
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYF_OA-dG63Txsd",
        "_score": null,
        "_source": {
          "name": "鲫鱼汤(变态辣)",
          "rating": 5,
          "type": "湘菜"
        },
        "fields": {
          "type": [
            "湘菜"
          ]
        },
        "sort": [
          5
        ],
        "inner_hits": {
          "top_rated": {
            "hits": {
              "total": 6,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYF_OA-dG63Txsd",
                  "_score": null,
                  "_source": {
                    "name": "鲫鱼汤(变态辣)",
                    "rating": 5,
                    "type": "湘菜"
                  },
                  "sort": [
                    5
                  ]
                },
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHX7_OA-dG63Txsc",
                  "_score": null,
                  "_source": {
                    "name": "鲫鱼汤(微辣)",
                    "rating": 4,
                    "type": "湘菜"
                  },
                  "sort": [
                    4
                  ]
                }
              ]
            }
          }
        }
      },
      {
        "_index": "recipes",
        "_type": "type",
        "_id": "AVoESHYW_OA-dG63Txse",
        "_score": null,
        "_source": {
          "name": "广式鲫鱼汤",
          "rating": 5,
          "type": "粤菜"
        },
        "fields": {
          "type": [
            "粤菜"
          ]
        },
        "sort": [
          5
        ],
        "inner_hits": {
          "top_rated": {
            "hits": {
              "total": 1,
              "max_score": null,
              "hits": [
                {
                  "_index": "recipes",
                  "_type": "type",
                  "_id": "AVoESHYW_OA-dG63Txse",
                  "_score": null,
                  "_source": {
                    "name": "广式鲫鱼汤",
                    "rating": 5,
                    "type": "粤菜"
                  },
                  "sort": [
                    5
                  ]
                }
              ]
            }
          }
        }
      }
    ]
  }
}

好了,字段折叠介绍就到这里。


https://elasticsearch.cn/question/263

<think>好的,我现在需要帮助用户解决如何使用Python对Elasticsearch进行复杂查询的问题。用户提到了四个具体需求:字段包含指定字符、两个字段同时包含相同字符、按相关性得分排序以及分页功能。首先,我得回顾下Elasticsearch的查询语法和Python客户端的使用方法。 首先,用户提到的第一个条件是某个字段包含指定字符。这应该可以用Elasticsearch的模糊查询或者匹配查询来实现。比如使用`match`查询或者`wildcard`。不过,用户提到的是“包含指定字符”,可能更倾向于模糊匹配或者通配符查询。比如,使用`wildcard`来匹配部分字符串,但要注意通配符查询的性能问题。或者使用`match`查询,结合分词器来处理。需要根据具体场景选择,但这里可能用`wildcard`更直接。 第二个条件是两个字段同时包含相同字符。这里可能需要使用多字段查询,比如`multi_match`查询,并设置相同查询词。或者使用`bool`查询的`must`子句,将两个字段的条件组合起来。比如,两个`wildcard`查询放在`must`里,但这样可能会有性能问题,尤其是如果字段数据量大。另外,是否允许用户输入的通配符在两个字段中同时存在?比如,用户输入“abc”,那么两个字段都要包含“abc”。这时候使用`bool`查询结合两个`wildcard`条件可能是正确的做法。 第三个条件是按相关性得分排序,高分在前。默认情况下,Elasticsearch的查询结果已经按相关性得分排序,但如果有自定义排序需求,可能需要使用`sort`参数,显式地按照`_score`降序排列。用户可能希望确保即使有其他排序条件,得分仍然是主要因素。所以需要在查询中添加`sort`参数,指定`_score`的降序。 第四个分页功能,这通常是通过`from`和`size`参数实现的。`from`指定起始位置,`size`指定每页大小。需要注意深度分页的问题,比如超过10000条数据时需要其他方法,但用户可能只需要基本的分页,所以暂时用`from`和`size`即可。 接下来,我需要将这些条件整合到一个Python的Elasticsearch查询中。Python的elasticsearch库使用`search`方法,构造查询体。可能需要用到`elasticsearch_dsl`库来更方便地构建查询,但用户可能使用的是低级别的客户端,所以需要构造一个字典结构的查询体。 现在,考虑第一个条件:某个字段包含指定字符。假设字段名为`field1`,查询词为`*query*`,使用`wildcard`查询。例如: {"wildcard": {"field1": {"value": "*query*"}}} 第二个条件,两个字段同时包含相同字符,比如`field1`和`field2`都包含`*query*`。这时候需要在`bool`的`must`中包含两个`wildcard`查询。 第三个条件,排序部分,添加`sort`字段,设置为`[{"_score": {"order": "desc"}}]`。 分页的话,添加`from`和`size`参数,比如`from=0`, `size=10`。 然后,将这些组合起来,形成一个完整的查询体。此外,用户可能需要同时满足其他过滤条件,比如范围查询或者其他条件,但用户的问题中没有提到,所以暂时不考虑。 另外,关于相关性得分,如果同时使用多个条件,Elasticsearch会自动计算综合得分。但用户可能需要确保这些条件的组合不会影响排序的正确性,可能需要调整boost参数,但用户没有提到,所以暂时保持默认。 现在,我需要确保这些条件在Python代码中的正确写法。例如,使用elasticsearch库的search方法: es.search(index="index_name", body={ "query": { "bool": { "must": [ {"wildcard": {"field1": {"value": "*query*"}}}, {"wildcard": {"field2": {"value": "*query*"}}} ] } }, "sort": [{"_score": {"order": "desc"}}], "from": 0, "size": 10 }) 不过,用户提到的是“两个字段同时包含相同字符”,这里的“相同字符”是否指的是同一个具体的值?例如,用户输入一个关键字,两个字段都必须包含它。如果是这样的话,上面的写法是正确的,每个wildcard查询使用相同的值。但如果是两个字段需要包含相同的字符(即两个字段的值中有相同的字符,但不定和用户输入相关),那可能需要不同的处理方式,比如脚本查询。但根据用户的问题描述,更可能是指用户输入一个字符,两个字段都要包含它,所以上面的写法是对的。 不过,使用`wildcard`可能会影响性能,特别是如果数据量大的话。但用户的需求明确,可能需要接受这点。或者建议用户使用其他查询方式,比如match_phrase,但需要确认字段是否被正确分析。 另外,是否应该使用`multi_match`查询?例如: { "query": { "multi_match": { "query": "query", "fields": ["field1", "field2"], "type": "phrase" } } } 不过,`multi_match`的`phrase`类型会要求字段中包含完整的查询短语,而不是部分字符。如果用户需要的是字段包含任意位置的指定字符(比如子字符串),那么wildcard是更合适的选择。但需要明确用户的需求是子字符串匹配还是分词后的匹配。 例如,如果字段值是“hello world”,用户查询“llo”,那么wildcard查询`*llo*`会匹配到,而match查询可能不会,除非分词器将整个字符串当作一个词项。所以用户的问题中的“包含指定字符”更可能指的是子字符串匹配,因此wildcard是正确的选择。 综上,构建的查询应该包括两个wildcard条件在bool must中,同时排序和分页参数。 现在,将这些转化为Python代码,并给出示例。同时,需要引用用户提供的参考内容。例如,参考[^3]中提到了模糊查询的使用,但这里用户的需求是wildcard,可能有所不同。但引用中的示例使用了QueryBuilders,可能在Java中,但Python中构造查询体类似。 此外,用户提供的引用[^1]和[^2]关于分片和路由,可能与查询无关,但引用[^4]是关于Python客户端的连接,所以在示例代码中需要展示如何建立连接,如使用es_ordinary的创建方式。 所以,代码示例可能包括连接部分,然后构建查询体,执行搜索,处理结果。 现在,总结步骤: 1. 建立Elasticsearch连接,使用用户提供的引用中的es_ordinary的创建方式。 2. 构建查询体,满足四个条件。 3. 执行查询,处理分页。 4. 输出结果。 然后,生成相关问题,可能涉及性能优化、分页限制、查询优化等。 最后,确保回答中的LaTeX格式正确,行内公式用$,独立公式用$$。但这个问题可能不涉及数学公式,所以可能不需要使用,但需要检查是否有需要的地方。 例如,在解释相关性得分时,可能需要提到评分公式,比如TF-IDF或BM25,这时候需要用LaTeX。例如,BM25的公式可以写成: $$ score(D, Q) = \sum_{t \in Q} IDF(t) \cdot \frac{f(t, D) \cdot (k_1 + 1)}{f(t, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{avgdl})} $$ 但用户的问题中没有要求深入解释评分机制,所以可能不需要。但如果有提到相关性排序,可能简要说明,并引用公式,但根据用户的问题,可能不需要。 综上,按照用户的需求,给出具体的Python代码示例,解释每个部分的作用,并引用相关的参考资料中的内容,如分片配置、路由等可能不直接相关,但连接部分可以引用。</think>以下是使用Python实现Elasticsearch复杂查询的完整方案: ### 、连接配置 首先建立Elasticsearch连接(基于引用的配置方式): ```python from elasticsearch import Elasticsearch es = Elasticsearch( hosts=["http://localhost:9200"], http_auth=("username", "password"), maxsize=25 ) ``` ### 二、复合查询实现 ```python def complex_search(index_name, search_term, page=1, page_size=10): body = { "query": { "bool": { "must": [ # 条件1:title字段包含指定字符(支持通配符) {"wildcard": {"title": {"value": f"*{search_term}*"}}}, # 条件2:title和content同时包含相同字符 {"multi_match": { "query": search_term, "fields": ["title", "content"], "type": "phrase" }} ] } }, # 条件3:按相关性得分降序排序 "sort": [{"_score": {"order": "desc"}], # 条件4:分页参数 "from": (page-1)*page_size, "size": page_size } return es.search(index=index_name, body=body) ``` ### 三、方案解析 1. **字段包含查询** 使用`wildcard`查询实现模糊匹配,`*{search_term}*`表示前后可有任意字符。例如搜索"py"可匹配"python" 2. **多字段联合查询** 采用`multi_match`查询,`type: "phrase"`确保在两个字段中找到完全匹配的词组。相关性得分计算基于BM25算法: $$ score(D,Q) = \sum_{t \in Q} IDF(t) \cdot \frac{f(t,D) \cdot (k_1 + 1)}{f(t,D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{avgdl})} $$ [^1] 3. **分页机制** 通过`from`和`size`参数实现数据分片访问,例如每页10条数据时: - 第页:from=0, size=10 - 第二页:from=10, size=10 ### 四、执行示例 ```python result = complex_search( index_name="articles", search_term="人工智能", page=2, page_size=15 ) # 输出命中文档 for hit in result['hits']['hits']: print(f"Score: {hit['_score']} | Title: {hit['_source']['title']}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值