| .. Licensed to the Apache Software Foundation (ASF) under one | |
| .. or more contributor license agreements. See the NOTICE file | |
| .. distributed with this work for additional information | |
| .. regarding copyright ownership. The ASF licenses this file | |
| .. to you under the Apache License, Version 2.0 (the | |
| .. "License"); you may not use this file except in compliance | |
| .. with the License. You may obtain a copy of the License at | |
| .. http://www.apache.org/licenses/LICENSE-2.0 | |
| .. Unless required by applicable law or agreed to in writing, | |
| .. software distributed under the License is distributed on an | |
| .. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| .. KIND, either express or implied. See the License for the | |
| .. specific language governing permissions and limitations | |
| .. under the License. | |
| ======= | |
| Dataset | |
| ======= | |
| .. warning:: | |
| Experimental: The Java module ``dataset`` is currently under early | |
| development. API might be changed in each release of Apache Arrow until it | |
| gets mature. | |
| Dataset is an universal layer in Apache Arrow for querying data in different | |
| formats or in different partitioning strategies. Usually the data to be queried | |
| is supposed to be located from a traditional file system, however Arrow Dataset | |
| is not designed only for querying files but can be extended to serve all | |
| possible data sources such as from inter-process communication or from other | |
| network locations, etc. | |
| Getting Started | |
| =============== | |
| Currently supported file formats are: | |
| - Apache Arrow (``.arrow``) | |
| - Apache ORC (``.orc``) | |
| - Apache Parquet (``.parquet``) | |
| - Comma-Separated Values (``.csv``) | |
| - Line-delimited JSON Values (``.json``) | |
| Below shows a simplest example of using Dataset to query a Parquet file in Java: | |
| .. code-block:: Java | |
| // read data from file /opt/example.parquet | |
| String uri = "file:/opt/example.parquet"; | |
| ScanOptions options = new ScanOptions(/*batchSize*/ 32768); | |
| try ( | |
| BufferAllocator allocator = new RootAllocator(); | |
| DatasetFactory datasetFactory = new FileSystemDatasetFactory( | |
| allocator, NativeMemoryPool.getDefault(), | |
| FileFormat.PARQUET, uri); | |
| Dataset dataset = datasetFactory.finish(); | |
| Scanner scanner = dataset.newScan(options); | |
| ArrowReader reader = scanner.scanBatches() | |
| ) { | |
| List<ArrowRecordBatch> batches = new ArrayList<>(); | |
| while (reader.loadNextBatch()) { | |
| try (VectorSchemaRoot root = reader.getVectorSchemaRoot()) { | |
| final VectorUnloader unloader = new VectorUnloader(root); | |
| batches.add(unloader.getRecordBatch()); | |
| } | |
| } | |
| // do something with read record batches, for example: | |
| analyzeArrowData(batches); | |
| // finished the analysis of the data, close all resources: | |
| AutoCloseables.close(batches); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } | |
| .. note:: | |
| ``ArrowRecordBatch`` is a low-level composite Arrow data exchange format | |
| that doesn't provide API to read typed data from it directly. | |
| It's recommended to use utilities ``VectorLoader`` to load it into a schema | |
| aware container ``VectorSchemaRoot`` by which user could be able to access | |
| decoded data conveniently in Java. | |
| The ``ScanOptions batchSize`` argument takes effect only if it is set to a value | |
| smaller than the number of rows in the recordbatch. | |
| .. seealso:: | |
| Load record batches with :doc:`VectorSchemaRoot <vector_schema_root>`. | |
| Schema | |
| ====== | |
| Schema of the data to be queried can be inspected via method | |
| ``DatasetFactory#inspect()`` before actually reading it. For example: | |
| .. code-block:: Java | |
| // read data from local file /opt/example.parquet | |
| String uri = "file:/opt/example.parquet"; | |
| BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); | |
| DatasetFactory factory = new FileSystemDatasetFactory(allocator, | |
| NativeMemoryPool.getDefault(), FileFormat.PARQUET, uri); | |
| // inspect schema | |
| Schema schema = factory.inspect(); | |
| For some of the data format that is compatible with a user-defined schema, user | |
| can use method ``DatasetFactory#inspect(Schema schema)`` to create the dataset: | |
| .. code-block:: Java | |
| Schema schema = createUserSchema() | |
| Dataset dataset = factory.finish(schema); | |
| Otherwise when the non-parameter method ``DatasetFactory#inspect()`` is called, | |
| schema will be inferred automatically from data source. The same as the result | |
| of ``DatasetFactory#inspect()``. | |
| Also, if projector is specified during scanning (see next section | |
| :ref:`java-dataset-projection`), the actual schema of output data can be got | |
| within method ``Scanner::schema()``: | |
| .. code-block:: Java | |
| Scanner scanner = dataset.newScan( | |
| new ScanOptions(32768, Optional.of(new String[] {"id", "name"}))); | |
| Schema projectedSchema = scanner.schema(); | |
| .. _java-dataset-projection: | |
| Projection (Subset of Columns) | |
| ============================== | |
| User can specify projections in ScanOptions. For example: | |
| .. code-block:: Java | |
| String[] projection = new String[] {"id", "name"}; | |
| ScanOptions options = new ScanOptions(32768, Optional.of(projection)); | |
| If no projection is needed, leave the optional projection argument absent in | |
| ScanOptions: | |
| .. code-block:: Java | |
| ScanOptions options = new ScanOptions(32768, Optional.empty()); | |
| Or use shortcut constructor: | |
| .. code-block:: Java | |
| ScanOptions options = new ScanOptions(32768); | |
| Then all columns will be emitted during scanning. | |
| Projection (Produce New Columns) and Filters | |
| ============================================ | |
| User can specify projections (new columns) or filters in ScanOptions using Substrait. For example: | |
| .. code-block:: Java | |
| ByteBuffer substraitExpressionFilter = getSubstraitExpressionFilter(); | |
| ByteBuffer substraitExpressionProject = getSubstraitExpressionProjection(); | |
| // Use Substrait APIs to create an Expression and serialize to a ByteBuffer | |
| ScanOptions options = new ScanOptions.Builder(/*batchSize*/ 32768) | |
| .columns(Optional.empty()) | |
| .substraitExpressionFilter(substraitExpressionFilter) | |
| .substraitExpressionProjection(getSubstraitExpressionProjection()) | |
| .build(); | |
| .. seealso:: | |
| :doc:`Executing Projections and Filters Using Extended Expressions <substrait>` | |
| Projections and Filters using Substrait. | |
| Read Data from HDFS | |
| =================== | |
| ``FileSystemDataset`` supports reading data from non-local file systems. HDFS | |
| support is included in the official Apache Arrow Java package releases and | |
| can be used directly without re-building the source code. | |
| To access HDFS data using Dataset API, pass a general HDFS URI to | |
| ``FilesSystemDatasetFactory``: | |
| .. code-block:: Java | |
| String uri = "hdfs://{hdfs_host}:{port}/data/example.parquet"; | |
| BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); | |
| DatasetFactory factory = new FileSystemDatasetFactory(allocator, | |
| NativeMemoryPool.getDefault(), FileFormat.PARQUET, uri); | |
| Native Memory Management | |
| ======================== | |
| To gain better performance and reduce code complexity, Java | |
| ``FileSystemDataset`` internally relies on C++ | |
| ``arrow::dataset::FileSystemDataset`` via JNI. | |
| As a result, all Arrow data read from ``FileSystemDataset`` is supposed to be | |
| allocated off the JVM heap. To manage this part of memory, an utility class | |
| ``NativeMemoryPool`` is provided to users. | |
| As a basic example, by using a listenable ``NativeMemoryPool``, user can pass | |
| a listener hooking on C++ buffer allocation/deallocation: | |
| .. code-block:: Java | |
| AtomicLong reserved = new AtomicLong(0L); | |
| ReservationListener listener = new ReservationListener() { | |
| @Override | |
| public void reserve(long size) { | |
| reserved.getAndAdd(size); | |
| } | |
| @Override | |
| public void unreserve(long size) { | |
| reserved.getAndAdd(-size); | |
| } | |
| }; | |
| NativeMemoryPool pool = NativeMemoryPool.createListenable(listener); | |
| FileSystemDatasetFactory factory = new FileSystemDatasetFactory(allocator, | |
| pool, FileFormat.PARQUET, uri); | |
| Also, it's a very common case to reserve the same amount of JVM direct memory | |
| for the data read from datasets. For this use a built-in utility | |
| class ``DirectReservationListener`` is provided: | |
| .. code-block:: Java | |
| NativeMemoryPool pool = NativeMemoryPool.createListenable( | |
| DirectReservationListener.instance()); | |
| This way, once the allocated byte count of Arrow buffers reaches the limit of | |
| JVM direct memory, ``OutOfMemoryError: Direct buffer memory`` will | |
| be thrown during scanning. | |
| .. note:: | |
| The default instance ``NativeMemoryPool.getDefaultMemoryPool()`` does | |
| nothing on buffer allocation/deallocation. It's OK to use it in | |
| the case of POC or testing, but for production use in complex environment, | |
| it's recommended to manage memory by using a listenable memory pool. | |
| .. note:: | |
| The ``BufferAllocator`` instance passed to ``FileSystemDatasetFactory``'s | |
| constructor is also aware of the overall memory usage of the produced | |
| dataset instances. Once the Java buffers are created the passed allocator | |
| will become their parent allocator. | |
| Usage Notes | |
| =========== | |
| Native Object Resource Management | |
| --------------------------------- | |
| As another result of relying on JNI, all components related to | |
| ``FileSystemDataset`` should be closed manually or use try-with-resources to | |
| release the corresponding native objects after using. For example: | |
| .. code-block:: Java | |
| String uri = "file:/opt/example.parquet"; | |
| ScanOptions options = new ScanOptions(/*batchSize*/ 32768); | |
| try ( | |
| BufferAllocator allocator = new RootAllocator(); | |
| DatasetFactory factory = new FileSystemDatasetFactory( | |
| allocator, NativeMemoryPool.getDefault(), | |
| FileFormat.PARQUET, uri); | |
| Dataset dataset = factory.finish(); | |
| Scanner scanner = dataset.newScan(options) | |
| ) { | |
| // do something | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } | |
| If user forgets to close them then native object leakage might be caused. | |
| BatchSize | |
| --------- | |
| The ``batchSize`` argument of ``ScanOptions`` is a limit on the size of an individual batch. | |
| For example, let's try to read a Parquet file with gzip compression and 3 row groups: | |
| .. code-block:: | |
| # Let configure ScanOptions as: | |
| ScanOptions options = new ScanOptions(/*batchSize*/ 32768); | |
| $ parquet-tools meta data4_3rg_gzip.parquet | |
| file schema: schema | |
| age: OPTIONAL INT64 R:0 D:1 | |
| name: OPTIONAL BINARY L:STRING R:0 D:1 | |
| row group 1: RC:4 TS:182 OFFSET:4 | |
| row group 2: RC:4 TS:190 OFFSET:420 | |
| row group 3: RC:3 TS:179 OFFSET:838 | |
| Here, we set the batchSize in ScanOptions to 32768. Because that's greater | |
| than the number of rows in the next batch, which is 4 rows because the first | |
| row group has only 4 rows, then the program gets only 4 rows. The scanner | |
| will not combine smaller batches to reach the limit, but it will split | |
| large batches to stay under the limit. So in the case the row group had more | |
| than 32768 rows, it would get split into blocks of 32768 rows or less. |