<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Quentin Pradet - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Quentin Pradet - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/search-labs/author/quentin-pradet</link>
    </image>
    <link>https://www.elastic.co/search-labs/author/quentin-pradet</link>
    <atom:link href="https://www.elastic.co/search-labs/rss/author/quentin-pradet.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[en]]></language>
    <lastBuildDate>Sun, 13 Sep 2026 09:25:53 GMT</lastBuildDate>
  <item>
    <title><![CDATA[From ES|QL to native Pandas dataframes in Python]]></title>
    <description><![CDATA[Learn how to export ES|QL queries as native Pandas dataframes in Python through practical examples.]]></description>
    <content:encoded><![CDATA[<p>Since Elasticsearch 8.15 or with Elasticsearch Serverless, <a href="https://github.com/elastic/elasticsearch/pull/109873">ES|QL responses support the Apache Arrow streaming format</a>. This blog post will show you how to take advantage of it in Python. In an <a href="https://www.elastic.co/search-labs/blog/esql-pandas-dataframes-python">earlier blog post</a>, I demonstrated how to convert ES|QL queries to Pandas dataframes using CSV as an intermediate representation. Unfortunately, CSV requires explicit type declarations, is slow (especially for larger datasets) and does not handle nested arrays and objects. Apache Arrow lifts all these limitations.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blta5d7fdd54f312f06/6a17d7bbfaa913809b93c6db/7decbe330061eae8108f7ad6a32a2df01f55244f-389x144.svg" alt="ES|QL produces tables" /><h2>ES|QL to Pandas dataframes in Python</h2><h3>Importing test data</h3><p>First, let's import some test data. As before, we will be using the <code>employees</code> <a href="https://github.com/elastic/elasticsearch/blob/d46bcc968e6cabca55f1a62b2218e9fc4e84e9d4/x-pack/plugin/esql/qa/testFixtures/src/main/resources/employees.csv">sample data</a> and <a href="https://github.com/elastic/elasticsearch/blob/main/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-default.json">mappings</a>. The easiest way to load this dataset is to <a href="https://gist.github.com/pquentin/7cf29a5932cf52b293699dd994b1a276">run these two Elasticsearch API requests</a> in the <a href="https://www.elastic.co/guide/en/kibana/current/console-kibana.html">Kibana Console</a>.</p><h3>Converting dataset to a Pandas DataFrame object</h3><p>OK, with that out of the way, let's convert the full <code>employees</code> dataset to a Pandas DataFrame object using the ES|QL Arrow export:</p>from elasticsearch import Elasticsearch
import pandas as pd

client = Elasticsearch(
    "https://[host].elastic-cloud.com",
    api_key="...",
)

response = client.esql.query(
    query="""
    FROM employees
    | DROP is_rehired,job_positions,salary_change*
    | LIMIT 500
    """,
    format="arrow",
)
df = response.to_pandas(types_mapper=pd.ArrowDtype)
print(df)
<p>Even though this dataset only contains 100 records, we use a <code>LIMIT</code> command to avoid ES|QL warning us about potentially missing records. This prints the following dataframe:</p>    avg_worked_seconds           birth_date  ...  salary still_hired
0            268728049  1953-09-02 00:00:00  ...   57305        True
1            328922887  1964-06-02 00:00:00  ...   56371        True
2            200296405  1959-12-03 00:00:00  ...   61805       False
3            311267831  1954-05-01 00:00:00  ...   36174        True
4            244294991  1955-01-21 00:00:00  ...   63528        True
..                 ...                  ...  ...     ...         ...
95           204381503  1954-09-16 00:00:00  ...   43889       False
96           206258084  1952-02-27 00:00:00  ...   71165       False
97           272392146  1961-09-23 00:00:00  ...   44817       False
98           377713748  1956-05-25 00:00:00  ...   73578        True
99           223910853  1953-04-21 00:00:00  ...   68431        True

[100 rows x 17 columns]
<p>OK, so what actually happened here?</p><ul><li><p>Given <code>format="arrow"</code>, Elasticsearch returns binary Arrow streaming data</p></li><li><p>The Elasticsearch Python client looks at the Content-Type header and creates a <a href="https://arrow.apache.org/docs/python/index.html">PyArrow object</a></p></li><li><p>Finally, PyArrow's <a href="https://arrow.apache.org/docs/python/pandas.html">Pandas integration</a> converts the PyArrow object to a Pandas dataframe.</p></li></ul><p>Note that the <code>types_mapper=pd.ArrowDtype</code> parameter asks Pandas to use a PyArrow backend instead of a NumPy backend, since the source data is PyArrow. While this backend is not enabled by default for compatibility reasons, it <a href="https://datapythonista.me/blog/pandas-20-and-the-arrow-revolution-part-i">has many advantages</a>: it handles missing values, is faster, more interopable and supports more types. (This is not a <a href="https://arrow.apache.org/docs/python/pandas.html#memory-usage-and-zero-copy">zero copy conversion</a>, however.)</p><p>For this example to work, the Pandas and PyArrow optional dependencies need to be installed. If you want to use another dataframe library such as Polars instead, you don't need Pandas and can directly use <a href="https://docs.pola.rs/api/python/stable/reference/api/polars.from_arrow.html"><code>polars.from_arrow</code></a> to create a Polars DataFrame from the PyArrow table returned by the Elasticsearch client.</p><p>One limitation is that Elasticsearch does not currently handle multi-valued fields, which is why we had to drop the <code>is_rehired</code>, <code>job_positions</code> and <code>salary_change</code> columns. This limitation will be lifted in a future version of Elasticsearch.</p><p>Anyway, you now have a Pandas dataframe that you can use to analyze your data further. But you can also continue massaging the data using ES|QL, which is particularly useful when queries return more than 10,000 rows, the current maximum number of rows that ES|QL queries can return.</p><h3>More complex queries</h3><p>In the next example, we're counting how many employees are speaking a given language by using <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-commands.html#esql-stats-by"><code>STATS ... BY</code></a> (not unlike <code>GROUP BY</code> in SQL). And then we sort the result with the <code>languages</code> column using <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-commands.html#esql-sort"><code>SORT</code></a>:</p>response = client.esql.query(
    query="""
    FROM employees
    | DROP is_rehired,job_positions,salary_change*
    | STATS count = COUNT(emp_no) BY languages
    | SORT languages
    | LIMIT 500
    """,
    format="arrow",
)

df = response.to_pandas(types_mapper=pd.ArrowDtype)
print(df)
<p>Unlike with CSV, we did not have to specify any types, as Arrow data already includes types. Here's the result:</p>   count  languages
0     15          1
1     19          2
2     17          3
3     18          4
4     21          5
5     10       &lt;NA&gt;
<p>21 employees speak 5 languages, wow! And 10 employees did not declare any spoken language. The missing value is denoted by <code>&lt;NA&gt;</code>, which is consistently used for missing data with the PyArrow backend. If we had used the NumPy backend instead, this column would have been converted to floats and the missing value would have been a confusing <code>NaN</code>, as <a href="https://pandas.pydata.org/docs/user_guide/missing_data.html">NumPy integers don't have any sentinel value for missing data</a>.</p><h3>Queries with parameters</h3><p>Finally, suppose that you want to expand the query from the previous section to only consider employees that speak N or more languages, with N being a variable parameter. For this we can use <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-rest.html#esql-rest-params">ES|QL's built-in support for parameters</a>, which eliminates the risk of an injection attack associated with manually assembling queries with variable parts:</p>response = client.esql.query(
    query="""
    FROM employees
    | DROP is_rehired,job_positions,salary_change*
    | STATS count = COUNT(emp_no) BY languages
    | WHERE languages &gt;= (?)
    | SORT languages
    | LIMIT 500
    """,
    format="arrow",
    params=[3],
)

df = response.to_pandas(types_mapper=pd.ArrowDtype)
print(df)
<p>which prints the following:</p>   count  languages
0     17          3
1     18          4
2     21          5
<h2>Conclusion</h2><p>As we saw, ES|QL's native Arrow support makes working with Pandas and other DataFrame libraries even nicer than using CSV and it will continue to improve over time, with the multi-value support coming in a future version of Elasticsearch.</p><h2>Additional resources</h2><p>If you want to learn more about ES|QL, the <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL documentation</a> is the best place to start. You can also check out <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/Boston-Celtics-Demo/celtics-esql-demo.ipynb">this other Python example using Boston Celtics data</a>. To know more about the Python Elasticsearch client itself, you can <a href="https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html">refer to the documentation</a>, ask a question <a href="https://discuss.elastic.co/tag/language-clients">on Discuss with the language-clients tag</a> or <a href="https://github.com/elastic/elasticsearch-py">open a new issue</a> if you found a bug or have a feature request. Thank you!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/esql-pandas-native-dataframes-python</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/esql-pandas-native-dataframes-python</guid>
    <category><![CDATA[ES|QL]]></category>
    <category><![CDATA[Python]]></category>
    <dc:creator><![CDATA[Quentin Pradet]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb1808f6b0c1b0ed3/6a17d7bcec0f89c6c35a644e/1b32822c3bf2ad216b21d819c5795f080b6e6cbf-500x500.png" length="0" type="image/png"/>
    <pubDate>Thu, 05 Sep 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>