From d901b12689207f23344af89349494758fd29c52b Mon Sep 17 00:00:00 2001 From: Daniel King Date: Wed, 23 Oct 2024 11:54:29 -0400 Subject: [PATCH 1/9] feat: expanded Python docs + Rust docs --- .../workflows/{python-docs.yml => docs.yml} | 3 +- docs/Makefile | 6 + docs/_static/example.parquet | Bin 0 -> 35631 bytes docs/_static/file-format-2024-10-23-1642.svg | 10 + docs/_static/style.css | 3 + docs/_static/vortex_spiral_logo.svg | 2 + .../_static/vortex_spiral_logo_dark_theme.svg | 2 + docs/{ => api}/dataset.rst | 10 + docs/api/dtype.rst | 27 ++ docs/api/encoding.rst | 26 ++ docs/api/expr.rst | 26 ++ docs/api/index.rst | 12 + docs/api/io.rst | 19 ++ docs/api/scalar.rst | 25 ++ docs/conf.py | 48 +++- docs/dtype.rst | 7 - docs/encoding.rst | 7 - docs/expr.rst | 6 - docs/file_format.rst | 76 ++++++ docs/guide.rst | 173 ++++++++++++ docs/index.rst | 68 ++++- docs/io.rst | 6 - docs/pyproject.toml | 7 +- docs/quickstart.rst | 199 ++++++++++++++ docs/scalar.rst | 6 - pyvortex/pyproject.toml | 1 + pyvortex/python/vortex/__init__.py | 1 + pyvortex/python/vortex/dataset.py | 247 +++++++++++++++++- pyvortex/python/vortex/encoding.py | 98 +++++-- pyvortex/src/array.rs | 56 ++-- pyvortex/src/compress.rs | 14 +- pyvortex/src/dtype.rs | 6 +- pyvortex/src/expr.rs | 10 +- pyvortex/src/io.rs | 28 +- pyvortex/src/scalar.rs | 6 +- pyvortex/test/test_dataset.py | 2 +- requirements-dev.lock | 5 +- requirements.lock | 5 +- 38 files changed, 1120 insertions(+), 133 deletions(-) rename .github/workflows/{python-docs.yml => docs.yml} (92%) create mode 100644 docs/_static/example.parquet create mode 100644 docs/_static/file-format-2024-10-23-1642.svg create mode 100644 docs/_static/style.css create mode 100644 docs/_static/vortex_spiral_logo.svg create mode 100644 docs/_static/vortex_spiral_logo_dark_theme.svg rename docs/{ => api}/dataset.rst (77%) create mode 100644 docs/api/dtype.rst create mode 100644 docs/api/encoding.rst create mode 100644 docs/api/expr.rst create mode 100644 docs/api/index.rst create mode 100644 docs/api/io.rst create mode 100644 docs/api/scalar.rst delete mode 100644 docs/dtype.rst delete mode 100644 docs/encoding.rst delete mode 100644 docs/expr.rst create mode 100644 docs/file_format.rst create mode 100644 docs/guide.rst delete mode 100644 docs/io.rst create mode 100644 docs/quickstart.rst delete mode 100644 docs/scalar.rst diff --git a/.github/workflows/python-docs.yml b/.github/workflows/docs.yml similarity index 92% rename from .github/workflows/python-docs.yml rename to .github/workflows/docs.yml index 01ee3d4494..c622e6324a 100644 --- a/.github/workflows/python-docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,4 @@ -name: Python docs +name: Python & Rust docs on: push: @@ -32,6 +32,7 @@ jobs: built_sha=$(git rev-parse HEAD) + rm -rf docs/_build/html/rust/CACHETAG.DIR docs/_build/html/rust/debug mv docs/_build/html /tmp/html git fetch origin diff --git a/docs/Makefile b/docs/Makefile index af06c736ab..0b9b72f77e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -18,3 +18,9 @@ help: # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +html: rust-html + +.PHONY: rust-html +rust-html: + cargo doc --no-deps --workspace --all-features --target-dir $(BUILDDIR)/html/rust diff --git a/docs/_static/example.parquet b/docs/_static/example.parquet new file mode 100644 index 0000000000000000000000000000000000000000..46c86166bf4d88f6fa4f43c67801383cc70266ea GIT binary patch literal 35631 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 + + + + + + + Statistics"Tables"SchemaFooterPostscriptData.........AABBStruct { names: ["A", "B"]; dtypes: [Primitive { I32; nullable: false }, Utf8 { nullable: false }]; nullable: false }RowOffsetChunkIndex012......MinMaxOne Metadata Table Per ColumnNullCount...TrueCountColumns Chunked{has_metadata=true}Flat { begin: u64, end: u64 }FlatFlatFlat1 Flat Layout (i.e., Byte Offset Range)Per Column ChunkColumn AColumn BFirst child contains the byte offsetsfor the column's metadata tableChunked...FlatFlatFlat Chunked{has_metadata=true}Example: a Layout with Row GroupsEOFVersion Info (4 bytes)Magic (4 bytes)EOF: fixed sizeof 8-bytes(forever)Schema OffsetLayout Offset...Postscript: compile-time known size;guaranteed to fitinto initial readLayoutRow Count...Footer: variable-sizemetadata that isnecessary for pruning& pushdown \ No newline at end of file diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 0000000000..2d137b32a9 --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,3 @@ +html .pst-navbar-icon { + font-size: 1.5rem; +} diff --git a/docs/_static/vortex_spiral_logo.svg b/docs/_static/vortex_spiral_logo.svg new file mode 100644 index 0000000000..026901c94f --- /dev/null +++ b/docs/_static/vortex_spiral_logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/_static/vortex_spiral_logo_dark_theme.svg b/docs/_static/vortex_spiral_logo_dark_theme.svg new file mode 100644 index 0000000000..0c4d52bab2 --- /dev/null +++ b/docs/_static/vortex_spiral_logo_dark_theme.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/dataset.rst b/docs/api/dataset.rst similarity index 77% rename from docs/dataset.rst rename to docs/api/dataset.rst index 848e6592ca..16d564868d 100644 --- a/docs/dataset.rst +++ b/docs/api/dataset.rst @@ -6,5 +6,15 @@ query engines like DuckDB and Polars. In particular, Vortex will read data propo number of rows passing a filter condition and the number of columns in a selection. For most Vortex encodings, this property holds true even when the filter condition specifies a single row. +.. autosummary:: + :nosignatures: + + ~vortex.dataset.VortexDataset + ~vortex.dataset.VortexScanner + +.. raw:: html + +
+ .. automodule:: vortex.dataset :members: diff --git a/docs/api/dtype.rst b/docs/api/dtype.rst new file mode 100644 index 0000000000..4f529feea9 --- /dev/null +++ b/docs/api/dtype.rst @@ -0,0 +1,27 @@ +Array Data Types +================ + +The logical types of the elements of an Array. Each logical type is implemented by a variety of +Array encodings which describe both a representation-as-bytes as well as how to apply operations on +that representation. + +.. autosummary:: + :nosignatures: + + ~vortex.dtype.DType + ~vortex.dtype.binary + ~vortex.dtype.bool + ~vortex.dtype.float + ~vortex.dtype.int + ~vortex.dtype.null + ~vortex.dtype.uint + ~vortex.dtype.utf8 + +.. raw:: html + +
+ +.. automodule:: vortex.dtype + :members: + :imported-members: + diff --git a/docs/api/encoding.rst b/docs/api/encoding.rst new file mode 100644 index 0000000000..3ec5cb449c --- /dev/null +++ b/docs/api/encoding.rst @@ -0,0 +1,26 @@ +Arrays +====== + +A Vortex array is a possibly compressed ordered set of homogeneously typed values. Each array has a +logical type and a physical encoding. The logical type describes the set of operations applicable to +the values of this array. The physical encoding describes how this array is realized in memory, on +disk, and over the wire and how to apply operations to that realization. + +.. autosummary:: + :nosignatures: + + ~vortex.encoding.array + ~vortex.encoding.compress + ~vortex.encoding.Array + +.. raw:: html + +
+ +.. autofunction:: vortex.encoding.array + +.. autofunction:: vortex.encoding.compress + +.. autoclass:: vortex.encoding.Array + :members: + :special-members: __len__ diff --git a/docs/api/expr.rst b/docs/api/expr.rst new file mode 100644 index 0000000000..3fd6ab3390 --- /dev/null +++ b/docs/api/expr.rst @@ -0,0 +1,26 @@ +Expressions +=========== + +Vortex expressions represent simple filtering conditions on the rows of a Vortex array. For example, +the following expression represents the set of rows for which the `age` column lies between 23 and +55: + +.. doctest:: + + >>> import vortex + >>> age = vortex.expr.column("age") + >>> (23 > age) & (age < 55) # doctest: +SKIP + +.. autosummary:: + :nosignatures: + + ~vortex.expr.column + ~vortex.expr.Expr + +.. raw:: html + +
+ +.. autofunction:: vortex.expr.column + +.. autoclass:: vortex.expr.Expr diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000000..b67c96ad6e --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,12 @@ +Python API +========== + +.. toctree:: + :maxdepth: 5 + + encoding + dtype + io + dataset + expr + scalar diff --git a/docs/api/io.rst b/docs/api/io.rst new file mode 100644 index 0000000000..42c7e0de94 --- /dev/null +++ b/docs/api/io.rst @@ -0,0 +1,19 @@ +Input and Output +================ + +Vortex arrays support reading and writing to local and remote file systems, including plain-old +HTTP, S3, Google Cloud Storage, and Azure Blob Storage. + +.. autosummary:: + :nosignatures: + + ~vortex.io.read + ~vortex.io.write + +.. raw:: html + +
+ +.. automodule:: vortex.io + :members: + :imported-members: diff --git a/docs/api/scalar.rst b/docs/api/scalar.rst new file mode 100644 index 0000000000..288673a5c1 --- /dev/null +++ b/docs/api/scalar.rst @@ -0,0 +1,25 @@ +Scalars +======= + +A scalar is a single atomic value like the integer ``1``, the string ``"hello"``, or the structure +``{"age": 55, "name": "Angela"}``. The :meth:`.Array.scalar_at` method +returns a native Python value when the cost of doing so is small. However, for larger values like +binary data, UTF-8 strings, variable-length lists, and structures, Vortex returns a zero-copy *view* +of the Array data. The ``into_python`` method of each view will copy the scalar into a native Python +value. + +.. autosummary:: + :nosignatures: + + ~vortex.scalar.Buffer + ~vortex.scalar.BufferString + ~vortex.scalar.VortexList + ~vortex.scalar.VortexStruct + +.. raw:: html + +
+ +.. automodule:: vortex.scalar + :members: + :imported-members: diff --git a/docs/conf.py b/docs/conf.py index 719854b5e1..0fe652707d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,8 +15,11 @@ extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx_design", ] templates_path = ["_templates"] @@ -24,10 +27,10 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), - "pyarrow": ("https://arrow.apache.org/docs/", None), - "pandas": ("https://pandas.pydata.org/docs/", None), - "numpy": ("https://numpy.org/doc/stable/", None), - "polars": ("https://docs.pola.rs/api/python/stable/", None), + "pyarrow": ("https://arrow.apache.org/docs", None), + "pandas": ("https://pandas.pydata.org/docs", None), + "numpy": ("https://numpy.org/doc/stable", None), + "polars": ("https://docs.pola.rs/api/python/stable", None), } nitpicky = True # ensures all :class:, :obj:, etc. links are valid @@ -38,4 +41,37 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "pydata_sphinx_theme" -# html_static_path = ['_static'] # no static files yet +html_static_path = ["_static"] +html_css_files = ["style.css"] # relative to _static/ + +# -- Options for PyData Theme ------------------------------------------------ +html_theme_options = { + "show_toc_level": 2, + "logo": { + "alt_text": "The Vortex logo.", + "text": "Vortex", + "image_light": "_static/vortex_spiral_logo.svg", + "image_dark": "_static/vortex_spiral_logo_dark_theme.svg", + }, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/spiraldb/vortex", + "icon": "fa-brands fa-github", + "type": "fontawesome", + }, + { + "name": "PyPI", + "url": "https://pypi.org/project/vortex-array", + "icon": "fa-brands fa-python", + "type": "fontawesome", + }, + ], + "header_links_before_dropdown": 3, +} +html_sidebars = { + # hide the primary (left-hand) sidebar on pages without sub-pages + "quickstart": [], + "guide": [], + "file_format": [], +} diff --git a/docs/dtype.rst b/docs/dtype.rst deleted file mode 100644 index 9c30bc80b9..0000000000 --- a/docs/dtype.rst +++ /dev/null @@ -1,7 +0,0 @@ -Array Data Types -================ - -.. automodule:: vortex.dtype - :members: - :imported-members: - diff --git a/docs/encoding.rst b/docs/encoding.rst deleted file mode 100644 index 8448777fba..0000000000 --- a/docs/encoding.rst +++ /dev/null @@ -1,7 +0,0 @@ -Arrays -====== - -.. automodule:: vortex.encoding - :members: - :imported-members: - :special-members: __len__ diff --git a/docs/expr.rst b/docs/expr.rst deleted file mode 100644 index 854aec35ce..0000000000 --- a/docs/expr.rst +++ /dev/null @@ -1,6 +0,0 @@ -Row Filter Expressions -====================== - -.. automodule:: vortex.expr - :members: - :imported-members: diff --git a/docs/file_format.rst b/docs/file_format.rst new file mode 100644 index 0000000000..9f3f6a2ccf --- /dev/null +++ b/docs/file_format.rst @@ -0,0 +1,76 @@ +File Format +=========== + +Intuition +--------- + +The Vortex file format has both *layouts*, which describe how different chunks of columns are stored +relative to one another, and *encodings* which describe the byte representation of a contiguous +sequence of values. A layout describes how to contiguously store one or more arrays as is necessary +for storing an array on disk or transmitting it over the wire. An encoding defines one binary +representation for memory, disk, and the wire. + +.. _file-format--layouts: + +Layouts +^^^^^^^ + +Vortex arrays have the same binary representation in-memory, on-disk, and over-the-wire; however, +all the rows of all the columns are not necessarily contiguously laid out. Vortex has three kinds of +*layouts* which recursively compose: the *flat layout*, the *column layout*, and the *chunked +layout*. + +The flat layout is a contiguous sequence of bytes. Any Vortex array encoding can be serialized into +the flat layout. + +The column layout lays out each column of a struct-typed array as a separate sequence of bytes. Each +column may or may not recursively use a chunked layout. Column layouts permit readers to push-down +column projections. + +The chunked layout lays out an array as a sequence of row chunks. Each chunk may have a different +size. A chunked layout permits reader to push-down row filters based on statistics which we describe +later. Note that, if the laid out array is a struct array, each column uses the same chunk +size. This is equivalent to Parquet's row groups. + +The layout: chunked of struct of chunked of flat, is essentially a Parquet layout with row groups in +which each column's values are contiguously stored in pages. The layout: struct of chunked of flat +eliminates row groups, retaining only pages. The layout struct of flat does not permit any row +filter push-down because each array is, to the layout, an opaque sequence of bytes. + +The chunked layout stores, per chunk, metadata necessary for effective row filtering such as +sortedness, constancy, the minimum value, the maximum value, and the number of null rows. Readers +consult these metadata tables to avoid reading chunks without relevant data. + +.. card:: + + .. figure:: _static/file-format-2024-10-23-1642.svg + :width: 800px + :alt: A schematic of the file format + + +++ + + The Vortex file format has five sections: data, statistics, schema, footer, and postscript. The + postscript describes the locating of the schema and layout which in turn describe how to + interpret the data and metadata. The schema describes the logical type. The metadata contains + information necessary for row filtering. + +.. _included-codecs: + +Encodings +^^^^^^^^^ + +- Most of the Arrow encodings. +- Chunked, a sequence of arrays. +- Constant, a value and a length. +- Sparse, a value plus a pair of arrays representing exceptions: an array of indices and of values. +- FastLanes Frame-of-Reference, BitPacking, and Delta. +- Fast Static Symbol Table (FSST). +- Adapative Lossless Floating Point (ALP). +- ALP Real Double (ALP-RD). +- ByteBool, one byte per Boolean value. +- ZigZag. + +Specification +------------- + +TODO! diff --git a/docs/guide.rst b/docs/guide.rst new file mode 100644 index 0000000000..9b20e188c0 --- /dev/null +++ b/docs/guide.rst @@ -0,0 +1,173 @@ +Guide +===== + +.. admonition:: Rustaceans + + See the `Vortex Rust documentation `_, for details on Vortex in Rust. + +Python +------ + +Construct a Vortex array from lists of simple Python values: + +.. doctest:: + + >>> import vortex + >>> vtx = vortex.array([1, 2, 3, 4]) + >>> vtx.dtype + int(64, False) + +Python's :obj:`None` represents a missing or null value and changes the dtype of the array from +non-nullable 64-bit integers to nullable 64-bit integers: + +.. doctest:: + + >>> vtx = vortex.array([1, 2, None, 4]) + >>> vtx.dtype + int(64, True) + +A list of :class:`dict` is converted to an array of structures. Missing values may appear at any +level: + +.. doctest:: + + >>> vtx = vortex.array([ + ... {'name': 'Joseph', 'age': 25}, + ... {'name': None, 'age': 31}, + ... {'name': 'Angela', 'age': None}, + ... {'name': 'Mikhail', 'age': 57}, + ... {'name': None, 'age': None}, + ... None, + ... ]) + >>> vtx.dtype + struct({"age": int(64, True), "name": utf8(True)}, True) + +:meth:`.Array.to_pylist` converts a Vortex array into a list of Python values. + +.. doctest:: + + >>> vtx.to_pylist() + [{'age': 25, 'name': 'Joseph'}, {'age': 31, 'name': None}, {'age': None, 'name': 'Angela'}, {'age': 57, 'name': 'Mikhail'}, {'age': None, 'name': None}, {'age': None, 'name': None}] + +Arrow +^^^^^ + +The :func:`~vortex.encoding.array` function constructs a Vortex array from an Arrow one without any +copies: + +.. doctest:: + + >>> import pyarrow as pa + >>> arrow = pa.array([1, 2, None, 3]) + >>> arrow.type + DataType(int64) + >>> vtx = vortex.array(arrow) + >>> vtx.dtype + int(64, True) + +:meth:`.Array.to_arrow_array` converts back to an Arrow array: + +.. doctest:: + + >>> vtx.to_arrow_array() + + [ + 1, + 2, + null, + 3 + ] + +If you have a struct array, use :meth:`.Array.to_arrow_table` to construct an Arrow table: + +.. doctest:: + + >>> struct_vtx = vortex.array([ + ... {'name': 'Joseph', 'age': 25}, + ... {'name': 'Narendra', 'age': 31}, + ... {'name': 'Angela', 'age': 33}, + ... {'name': 'Mikhail', 'age': 57}, + ... ]) + >>> struct_vtx.to_arrow_table() + pyarrow.Table + age: int64 + name: string_view + ---- + age: [[25,31,33,57]] + name: [["Joseph","Narendra","Angela","Mikhail"]] + +Pandas +^^^^^^ + +:meth:`.Array.to_pandas_df` converts a Vortex array into a Pandas DataFrame: + +.. doctest:: + + >>> df = struct_vtx.to_pandas_df() + >>> df + age name + 0 25 Joseph + 1 31 Narendra + 2 33 Angela + 3 57 Mikhail + +:func:`~vortex.encoding.array` converts from a Pandas DataFrame into a Vortex array: + + >>> vortex.array(df).to_arrow_table() + pyarrow.Table + age: int64 + name: string_view + ---- + age: [[25,31,33,57]] + name: [["Joseph","Narendra","Angela","Mikhail"]] + + +.. _query-engine-integration: + +Query Engines +------------- + +:class:`~vortex.dataset.VortexDataset` implements the :class:`pyarrow.dataset.Dataset` API which +enables many Python-based query engines to pushdown row filters and column projections on Vortex +files. + +Polars +^^^^^^ + + >>> import polars as pl + >>> ds = vortex.dataset.dataset( + ... '_static/example.vortex' + ... ) + >>> lf = pl.scan_pyarrow_dataset(ds) + >>> lf = lf.select('tip_amount', 'fare_amount') + >>> lf = lf.head(3) + >>> lf.collect() + shape: (3, 2) + ┌────────────┬─────────────┐ + │ tip_amount ┆ fare_amount │ + │ --- ┆ --- │ + │ f64 ┆ f64 │ + ╞════════════╪═════════════╡ + │ 0.0 ┆ 61.8 │ + │ 5.1 ┆ 20.5 │ + │ 16.54 ┆ 70.0 │ + └────────────┴─────────────┘ + +DuckDB +^^^^^^ + + >>> import duckdb + >>> ds = vortex.dataset.dataset( + ... '_static/example.vortex' + ... ) + >>> duckdb.sql('select ds.tip_amount, ds.fare_amount from ds limit 3').show() + ┌────────────┬─────────────┐ + │ tip_amount │ fare_amount │ + │ double │ double │ + ├────────────┼─────────────┤ + │ 0.0 │ 61.8 │ + │ 5.1 │ 20.5 │ + │ 16.54 │ 70.0 │ + └────────────┴─────────────┘ + + diff --git a/docs/index.rst b/docs/index.rst index c89a19c9a3..b327422fc8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,18 +3,64 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Vortex documentation -==================== +Wide, Fast & Compact. Pick Three. +================================== -Vortex is an Apache Arrow-compatible toolkit for working with compressed array data. +.. grid:: 1 1 2 2 + :gutter: 4 4 4 4 + + .. grid-item-card:: Row groups? Your choice. + :link: file-format--layouts + :link-type: ref + + Bring your wide schemas, images, and videos. + + .. grid-item-card:: All your favorite query engines. + :link: query-engine-integration + :link-type: ref + + Query pushdown in Pandas, Polars, DuckDB, & chDB. + + .. grid-item-card:: 200x lower latency random reads. + + Block compression is for chumps. + + .. grid-item-card:: Zero copy reads. + + It's called a processor, not a Xerox. + + .. grid-item-card:: Batteries-included. + :columns: 12 12 12 12 + :link: included-codecs + :link-type: ref + + Cutting-edge codecs: FSST, ALP, FastLanes, and more. + +Vortex is a fast, extensible, lightweight-compressed, and random-access columnar file format as well +as a library for working with compressed Apache Arrow arrays in-memory, on-disk, and +over-the-wire. Vortex aspires to succeed Apache Parquet by delivering two orders of magnitude faster +random-access and faster scans without sacrificing compression ratio nor write throughput. Its +features include: + +- A zero-copy data layout for disk, memory, and the wire. +- Kernels for computing on, filtering, slicing, indexing, and projecting compressed arrays. +- Builtin state-of-the-art codecs including FastLanes (integer bit-packing), ALP (floating point), + and FSST (strings). +- Support for custom user-implemented codecs. +- Support for, but no requirement for, row groups. +- A read sub-system supporting filter and projection pushdown. + +Spiral's flexible layout empowers writers to choose the right layout for their setting: fast writes, +fast reads, small files, few columns, many columns, over-sized columns, etc. + +Documentation +------------- .. toctree:: :maxdepth: 2 - :caption: Contents: - - encoding - dtype - io - dataset - expr - scalar + + quickstart + guide + file_format + api/index + Rust API diff --git a/docs/io.rst b/docs/io.rst deleted file mode 100644 index f2cc405ce9..0000000000 --- a/docs/io.rst +++ /dev/null @@ -1,6 +0,0 @@ -Input and Output -================ - -.. automodule:: vortex.io - :members: - :imported-members: diff --git a/docs/pyproject.toml b/docs/pyproject.toml index 53ceee91e1..a93d7459ed 100644 --- a/docs/pyproject.toml +++ b/docs/pyproject.toml @@ -3,7 +3,12 @@ name = "docs" version = "0.1.0" description = "Vortex documentation." authors = [] -dependencies = ["pydata-sphinx-theme>=0.15.4", "sphinx>=8.0.2", "pyvortex"] +dependencies = [ + "pydata-sphinx-theme>=0.16.0", + "sphinx>=8.0.2", + "pyvortex", + "sphinx-design>=0.6.1", +] requires-python = ">= 3.10" [tool.uv] diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000000..0c4018327c --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,199 @@ +Quickstart +========== + +The reference implementation exposes both a Rust and Python API. A C API is currently in progress. + +- :ref:`Quickstart for Python ` +- :ref:`Quickstart for Rust ` +- :ref:`Quickstart for C ` + +.. _python-quickstart: + +Python +------ + +Install +^^^^^^^ + +:: + + pip install vortex-array + +Convert +^^^^^^^ + +You can either use your own Parquet file or download the `example used here +`__. + +Use Arrow to read a Parquet file and then use :func:`~vortex.encoding.array` to construct an uncompressed +Vortex array: + +.. doctest:: + + >>> import pyarrow.parquet as pq + >>> import vortex + >>> parquet = pq.read_table("_static/example.parquet") + >>> vtx = vortex.array(parquet) + >>> vtx.nbytes + 141024 + +Compress +^^^^^^^^ + +Use :func:`~vortex.encoding.compress` to compress the Vortex array and check the relative size: + +.. doctest:: + + >>> cvtx = vortex.compress(vtx) + >>> cvtx.nbytes + 16174 + >>> cvtx.nbytes / vtx.nbytes + 0.11468969820739733 + +Vortex uses nearly ten times fewer bytes than Arrow. Fewer bytes means more of your data fits in +cache and RAM. + +Write +^^^^^ + +Use :func:`~vortex.io.write` to write the Vortex array to disk: + +.. doctest:: + + >>> vortex.io.write(cvtx, "example.vortex") + +Small Vortex files (this one is just 71KiB) currently have substantial overhead relative to their +size. This will be addressed shortly. On files with at least tens of megabytes of data, Vortex is +similar to or smaller than Parquet. + +.. doctest:: + + >>> from os.path import getsize + >>> getsize("example.vortex") / getsize("_static/example.parquet") + 2.17... + +Read +^^^^ + +Use :func:`~vortex.io.read` to read the Vortex array from disk: + +.. doctest:: + + >>> cvtx = vortex.io.read("example.vortex") + +.. _rust-quickstart: + +Rust +---- + +Install +^^^^^^^ + +Install vortex and all the first-party array encodings:: + + cargo add vortex-array vortex-alp vortex-fsst vortex-fastlanes \ + vortex-bytebool vortex-datetime-dtype vortex-datetime-parts \ + vortex-dict vortex-runend vortex-runend-bool vortex-zigzag \ + vortex-sampling-compressor vortex-serde + +Convert +^^^^^^^ + +You can either use your own Parquet file or download the `example used here +`__. + +Use Arrow to read a Parquet file and then construct an uncompressed Vortex array: + +.. code-block:: rust + + use std::fs::File; + + use arrow_array::RecordBatchReader; + use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; + use vortex::array::ChunkedArray; + use vortex::arrow::FromArrowType; + use vortex::{Array, IntoArray}; + use vortex_dtype::DType; + + let reader = + ParquetRecordBatchReaderBuilder::try_new(File::open("_static/example.parquet").unwrap()) + .unwrap() + .build() + .unwrap(); + let dtype = DType::from_arrow(reader.schema()); + let chunks = reader + .map(|x| Array::try_from(x.unwrap()).unwrap()) + .collect::>(); + let vtx = ChunkedArray::try_new(chunks, dtype).unwrap().into_array(); + +Compress +^^^^^^^^ + +Use the sampling compressor to compress the Vortex array and check the relative size: + +.. code-block:: rust + + use std::collections::HashSet; + + use vortex_sampling_compressor::{SamplingCompressor, DEFAULT_COMPRESSORS}; + + let compressor = SamplingCompressor::new(HashSet::from(*DEFAULT_COMPRESSORS)); + let cvtx = compressor.compress(&vtx, None).unwrap().into_array(); + println!("{}", cvtx.nbytes()); + +Write +^^^^^ + +Reading and writing both require an async runtime, in this example we use Tokio. The LayoutWriter +knows how to write Vortex arrays to disk: + +.. code-block:: rust + + use std::path::Path; + + use tokio::fs::File as TokioFile; + use vortex_serde::layouts::LayoutWriter; + + let file = TokioFile::create(Path::new("example.vortex")) + .await + .unwrap(); + let writer = LayoutWriter::new(file) + .write_array_columns(cvtx.clone()) + .await + .unwrap(); + writer.finalize().await.unwrap(); + +Read +^^^^ + +.. code-block:: rust + + use futures::TryStreamExt; + use vortex_sampling_compressor::ALL_COMPRESSORS_CONTEXT; + use vortex_serde::layouts::{LayoutContext, LayoutDeserializer, LayoutReaderBuilder}; + + let file = TokioFile::open(Path::new("example.vortex")).await.unwrap(); + let builder = LayoutReaderBuilder::new( + file, + LayoutDeserializer::new( + ALL_COMPRESSORS_CONTEXT.clone(), + LayoutContext::default().into(), + ), + ); + + let stream = builder.build().await.unwrap(); + let dtype = stream.schema().clone().into(); + let vecs: Vec = stream.try_collect().await.unwrap(); + let cvtx = ChunkedArray::try_new(vecs, dtype) + .unwrap() + .into_array(); + + println!("{}", cvtx.nbytes()); + + +.. _c-quickstart: + +C +- + +Coming soon! diff --git a/docs/scalar.rst b/docs/scalar.rst deleted file mode 100644 index 9fb3b26cfc..0000000000 --- a/docs/scalar.rst +++ /dev/null @@ -1,6 +0,0 @@ -Scalar Values -============= - -.. automodule:: vortex.scalar - :members: - :imported-members: diff --git a/pyvortex/pyproject.toml b/pyvortex/pyproject.toml index f482ce0541..08f78c3ceb 100644 --- a/pyvortex/pyproject.toml +++ b/pyvortex/pyproject.toml @@ -41,4 +41,5 @@ features = ["pyo3/extension-module"] include = [ { path = "rust-toolchain.toml", format = "sdist" }, { path = "README.md", format = "sdist" }, + { path = "python/vortex/py.typed", format = "sdist" }, ] diff --git a/pyvortex/python/vortex/__init__.py b/pyvortex/python/vortex/__init__.py index b7101a7ccc..6a50c5978b 100644 --- a/pyvortex/python/vortex/__init__.py +++ b/pyvortex/python/vortex/__init__.py @@ -5,5 +5,6 @@ __doc__ = module_docs del module_docs array = encoding.array +compress = encoding.compress __all__ = ["array", dtype, expr, io, encoding, scalar, dataset] diff --git a/pyvortex/python/vortex/dataset.py b/pyvortex/python/vortex/dataset.py index d64d7d803c..e5af91b584 100644 --- a/pyvortex/python/vortex/dataset.py +++ b/pyvortex/python/vortex/dataset.py @@ -7,16 +7,21 @@ import pyarrow.dataset from . import encoding -from ._lib import dataset +from ._lib import dataset as dataset_rs from .arrow.expression import arrow_to_vortex as arrow_to_vortex_expr class VortexDataset(pyarrow.dataset.Dataset): - """Read Vortex files with row filter and column selection pushdown.""" + """Read Vortex files with row filter and column selection pushdown. + + This class implements the :class:`.pyarrow.dataset.Dataset` interface which enables its use with + Polars, DuckDB, Pandas and others. + + """ def __init__(self, fname: str): self._fname = fname - self._dataset = dataset.dataset(fname) + self._dataset = dataset_rs.dataset(fname) @property def schema(self) -> pa.Schema: @@ -55,6 +60,35 @@ def head( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.Table: + """Load the first `num_rows` of the dataset. + + Parameters + ---------- + num_rows : int + The number of rows to load. + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ if batch_readahead is not None: raise ValueError("batch_readahead not supported") if fragment_readahead is not None: @@ -103,7 +137,33 @@ def scanner( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.dataset.Scanner: - """Not implemented.""" + """Construct a :class:`.pyarrow.dataset.Scanner`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return VortexScanner( self, columns, @@ -132,6 +192,35 @@ def take( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.Table: + """Load a subset of rows identified by their absolute indices. + + Parameters + ---------- + indices : :class:`.pyarrow.Array` + A numeric array of absolute indices into `self` indicating which rows to keep. + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_array(columns, batch_size, filter).take(encoding.array(indices)).to_arrow_table() def to_record_batch_reader( @@ -145,6 +234,33 @@ def to_record_batch_reader( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.RecordBatchReader: + """Construct a :class:`.pyarrow.RecordBatchReader`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ if batch_readahead is not None: raise ValueError("batch_readahead not supported") if fragment_readahead is not None: @@ -171,6 +287,33 @@ def to_batches( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> Iterator[pa.RecordBatch]: + """Construct an iterator of :class:`.pyarrow.RecordBatch`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ record_batch_reader = self.to_record_batch_reader( columns, filter, @@ -198,6 +341,33 @@ def to_table( use_threads: bool | None = None, memory_pool: pa.MemoryPool = None, ) -> pa.Table: + """Construct an Arrow :class:`.pyarrow.Table`. + + Parameters + ---------- + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ if batch_readahead is not None: raise ValueError("batch_readahead not supported") if fragment_readahead is not None: @@ -214,8 +384,40 @@ def to_table( return self._dataset.to_array(columns, batch_size, filter).to_arrow_table() +def dataset(fname: str) -> VortexDataset: + return VortexDataset(fname) + + class VortexScanner(pa.dataset.Scanner): - """A PyArrow Dataset Scanner that reads from a Vortex Array.""" + """A PyArrow Dataset Scanner that reads from a Vortex Array. + + Parameters + ---------- + dataset : VortexDataset + The dataset to scan. + columns : list of str + The columns to keep, identified by name. + filter : :class:`.pyarrow.dataset.Expression` + Keep only rows for which this expression evalutes to ``True``. Any rows for which + this expression evaluates to ``Null`` is removed. + batch_size : int + The maximum number of rows per batch. + batch_readahead : int + Not implemented. + fragment_readahead : int + Not implemented. + fragment_scan_options : :class:`.pyarrow.dataset.FragmentScanOptions` + Not implemented. + use_threads : bool + Not implemented. + memory_pool : :class:`.pyarrow.MemoryPool` + Not implemented. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ def __init__( self, @@ -255,6 +457,18 @@ def count_rows(self): ) def head(self, num_rows: int) -> pa.Table: + """Load the first `num_rows` of the dataset. + + Parameters + ---------- + num_rows : int + The number of rows to read. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.head( num_rows, self._columns, @@ -272,6 +486,13 @@ def scan_batches(self) -> Iterator[pa.dataset.TaggedRecordBatch]: raise NotImplementedError("scan batches") def to_batches(self) -> Iterator[pa.RecordBatch]: + """Construct an iterator of :class:`.pyarrow.RecordBatch`. + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_batches( self._columns, self._filter, @@ -284,6 +505,14 @@ def to_batches(self) -> Iterator[pa.RecordBatch]: ) def to_reader(self) -> pa.RecordBatchReader: + """Construct a :class:`.pyarrow.RecordBatchReader`. + + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_record_batch_reader( self._columns, self._filter, @@ -296,6 +525,14 @@ def to_reader(self) -> pa.RecordBatchReader: ) def to_table(self) -> pa.Table: + """Construct an Arrow :class:`.pyarrow.Table`. + + + Returns + ------- + table : :class:`.pyarrow.Table` + + """ return self._dataset.to_table( self._columns, self._filter, diff --git a/pyvortex/python/vortex/encoding.py b/pyvortex/python/vortex/encoding.py index ac522d3750..d36401af64 100644 --- a/pyvortex/python/vortex/encoding.py +++ b/pyvortex/python/vortex/encoding.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pandas import pyarrow @@ -61,7 +61,7 @@ def _Array_to_arrow_table(self: _encoding.Array) -> pyarrow.Table: Examples -------- - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, @@ -82,7 +82,7 @@ def _Array_to_arrow_table(self: _encoding.Array) -> pyarrow.Table: Array.to_arrow_table = _Array_to_arrow_table -def _Array_to_pandas(self: _encoding.Array) -> "pandas.DataFrame": +def _Array_to_pandas_df(self: _encoding.Array) -> "pandas.DataFrame": """Construct a Pandas dataframe from this Vortex array. Warning @@ -99,27 +99,24 @@ def _Array_to_pandas(self: _encoding.Array) -> "pandas.DataFrame": Construct a dataframe from a Vortex array: - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, ... {'name': 'Mikhail', 'age': 57}, ... ]) - >>> array.to_pandas() + >>> array.to_pandas_df() age name 0 25 Joseph 1 31 Narendra 2 33 Angela 3 57 Mikhail - - Lift the struct fields to the top-level in the dataframe: - """ return self.to_arrow_table().to_pandas(types_mapper=pandas.ArrowDtype) -Array.to_pandas = _Array_to_pandas +Array.to_pandas_df = _Array_to_pandas_df def _Array_to_polars_dataframe( @@ -146,7 +143,7 @@ def _Array_to_polars_dataframe( Examples -------- - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, @@ -193,7 +190,7 @@ def _Array_to_polars_series(self: _encoding.Array): # -> 'polars.Series': # br Convert a numeric array with nulls to a Polars Series: - >>> vortex.encoding.array([1, None, 2, 3]).to_polars_series() # doctest: +NORMALIZE_WHITESPACE + >>> vortex.array([1, None, 2, 3]).to_polars_series() # doctest: +NORMALIZE_WHITESPACE shape: (4,) Series: '' [i64] [ @@ -205,7 +202,7 @@ def _Array_to_polars_series(self: _encoding.Array): # -> 'polars.Series': # br Convert a UTF-8 string array to a Polars Series: - >>> vortex.encoding.array(['hello, ', 'is', 'it', 'me?']).to_polars_series() # doctest: +NORMALIZE_WHITESPACE + >>> vortex.array(['hello, ', 'is', 'it', 'me?']).to_polars_series() # doctest: +NORMALIZE_WHITESPACE shape: (4,) Series: '' [str] [ @@ -217,7 +214,7 @@ def _Array_to_polars_series(self: _encoding.Array): # -> 'polars.Series': # br Convert a struct array to a Polars Series: - >>> array = vortex.encoding.array([ + >>> array = vortex.array([ ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, @@ -262,7 +259,7 @@ def _Array_to_numpy(self: _encoding.Array, *, zero_copy_only: bool = True) -> "n Construct an immutable ndarray from a Vortex array: - >>> array = vortex.encoding.array([1, 0, 0, 1]) + >>> array = vortex.array([1, 0, 0, 1]) >>> array.to_numpy() array([1, 0, 0, 1]) @@ -273,14 +270,40 @@ def _Array_to_numpy(self: _encoding.Array, *, zero_copy_only: bool = True) -> "n Array.to_numpy = _Array_to_numpy -def array(obj: pyarrow.Array | list) -> Array: +def _Array_to_pylist(self: _encoding.Array) -> list[Any]: + """Deeply copy an Array into a Python list. + + Returns + ------- + :class:`list` + + Examples + -------- + + >>> array = vortex.array([ + ... {'name': 'Joseph', 'age': 25}, + ... {'name': 'Narendra', 'age': 31}, + ... {'name': 'Angela', 'age': 33}, + ... {'name': 'Mikhail', 'age': 57}, + ... ]) + >>> array.to_pylist() + [{'age': 25, 'name': 'Joseph'}, {'age': 31, 'name': 'Narendra'}, {'age': 33, 'name': 'Angela'}, {'age': 57, 'name': 'Mikhail'}] + + """ + return self.to_arrow_table().to_pylist() + + +Array.to_pylist = _Array_to_pylist + + +def array(obj: pyarrow.Array | list | Any) -> Array: """The main entry point for creating Vortex arrays from other Python objects. This function is also available as ``vortex.array``. Parameters ---------- - obj : :class:`pyarrow.Array` or :class:`list` + obj : :class:`pyarrow.Array`, :class:`list`, :class:`pandas.DataFrame` The elements of this array or list become the elements of the Vortex array. Returns @@ -290,9 +313,9 @@ def array(obj: pyarrow.Array | list) -> Array: Examples -------- - A Vortex array containing the first three integers. + A Vortex array containing the first three integers: - >>> vortex.encoding.array([1, 2, 3]).to_arrow_array() + >>> vortex.array([1, 2, 3]).to_arrow_array() [ 1, @@ -300,9 +323,9 @@ def array(obj: pyarrow.Array | list) -> Array: 3 ] - The same Vortex array with a null value in the third position. + The same Vortex array with a null value in the third position: - >>> vortex.encoding.array([1, 2, None, 3]).to_arrow_array() + >>> vortex.array([1, 2, None, 3]).to_arrow_array() [ 1, @@ -314,7 +337,7 @@ def array(obj: pyarrow.Array | list) -> Array: Initialize a Vortex array from an Arrow array: >>> arrow = pyarrow.array(['Hello', 'it', 'is', 'me']) - >>> vortex.encoding.array(arrow).to_arrow_array() + >>> vortex.array(arrow).to_arrow_array() [ "Hello", @@ -323,7 +346,40 @@ def array(obj: pyarrow.Array | list) -> Array: "me" ] + Initialize a Vortex array from a Pandas dataframe: + + >>> import pandas as pd + >>> df = pd.DataFrame({ + ... "Name": ["Braund", "Allen", "Bonnell"], + ... "Age": [22, 35, 58], + ... }) + >>> vortex.array(df).to_arrow_array() + + [ + -- is_valid: all not null + -- child 0 type: string_view + [ + "Braund", + "Allen", + "Bonnell" + ] + -- child 1 type: int64 + [ + 22, + 35, + 58 + ] + ] + """ + if isinstance(obj, list): return _encoding._encode(pyarrow.array(obj)) + try: + import pandas + + if isinstance(obj, pandas.DataFrame): + return _encoding._encode(pyarrow.Table.from_pandas(obj)) + except ImportError: + pass return _encoding._encode(obj) diff --git a/pyvortex/src/array.rs b/pyvortex/src/array.rs index 3daf16e2f3..1c47f008da 100644 --- a/pyvortex/src/array.rs +++ b/pyvortex/src/array.rs @@ -21,8 +21,8 @@ use crate::scalar::scalar_into_py; /// /// Arrays support all the standard comparison operations: /// -/// >>> a = vortex.encoding.array(['dog', None, 'cat', 'mouse', 'fish']) -/// >>> b = vortex.encoding.array(['doug', 'jennifer', 'casper', 'mouse', 'faust']) +/// >>> a = vortex.array(['dog', None, 'cat', 'mouse', 'fish']) +/// >>> b = vortex.array(['doug', 'jennifer', 'casper', 'mouse', 'faust']) /// >>> (a < b).to_arrow_array() /// /// [ @@ -107,7 +107,7 @@ impl PyArray { /// /// Round-trip an Arrow array through a Vortex array: /// - /// >>> vortex.encoding.array([1, 2, 3]).to_arrow_array() + /// >>> vortex.array([1, 2, 3]).to_arrow_array() /// /// [ /// 1, @@ -183,19 +183,19 @@ impl PyArray { /// Examples /// -------- /// - /// By default, :func:`vortex.encoding.array` uses the largest available bit-width: + /// By default, :func:`~vortex.encoding.array` uses the largest available bit-width: /// - /// >>> vortex.encoding.array([1, 2, 3]).dtype + /// >>> vortex.array([1, 2, 3]).dtype /// int(64, False) /// /// Including a :obj:`None` forces a nullable type: /// - /// >>> vortex.encoding.array([1, None, 2, 3]).dtype + /// >>> vortex.array([1, None, 2, 3]).dtype /// int(64, True) /// /// A UTF-8 string array: /// - /// >>> vortex.encoding.array(['hello, ', 'is', 'it', 'me?']).dtype + /// >>> vortex.array(['hello, ', 'is', 'it', 'me?']).dtype /// utf8(False) #[getter] fn dtype(self_: PyRef) -> PyResult> { @@ -254,19 +254,19 @@ impl PyArray { /// /// Parameters /// ---------- - /// filter : :class:`vortex.encoding.Array` + /// filter : :class:`~vortex.encoding.Array` /// Keep all the rows in ``self`` for which the correspondingly indexed row in `filter` is True. /// /// Returns /// ------- - /// :class:`vortex.encoding.Array` + /// :class:`~vortex.encoding.Array` /// /// Examples /// -------- /// /// Keep only the single digit positive integers. /// - /// >>> a = vortex.encoding.array([0, 42, 1_000, -23, 10, 9, 5]) + /// >>> a = vortex.array([0, 42, 1_000, -23, 10, 9, 5]) /// >>> filter = vortex.array([True, False, False, False, False, True, True]) /// >>> a.filter(filter).to_arrow_array() /// @@ -291,7 +291,7 @@ impl PyArray { /// Fill forward sensor values over intermediate missing values. Note that leading nulls are /// replaced with 0.0: /// - /// >>> a = vortex.encoding.array([ + /// >>> a = vortex.array([ /// ... None, None, 30.29, 30.30, 30.30, None, None, 30.27, 30.25, /// ... 30.22, None, None, None, None, 30.12, 30.11, 30.11, 30.11, /// ... 30.10, 30.08, None, 30.21, 30.03, 30.03, 30.05, 30.07, 30.07, @@ -347,12 +347,12 @@ impl PyArray { /// /// Retrieve the last element from an array of integers: /// - /// >>> vortex.encoding.array([10, 42, 999, 1992]).scalar_at(3) + /// >>> vortex.array([10, 42, 999, 1992]).scalar_at(3) /// 1992 /// /// Retrieve the third element from an array of strings: /// - /// >>> array = vortex.encoding.array(["hello", "goodbye", "it", "is"]) + /// >>> array = vortex.array(["hello", "goodbye", "it", "is"]) /// >>> array.scalar_at(2) /// /// @@ -365,7 +365,7 @@ impl PyArray { /// /// Retrieve an element from an array of structures: /// - /// >>> array = vortex.encoding.array([ + /// >>> array = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': 'Narendra', 'age': 31}, /// ... {'name': 'Angela', 'age': 33}, @@ -389,7 +389,7 @@ impl PyArray { /// /// Out of bounds accesses are prohibited: /// - /// >>> vortex.encoding.array([10, 42, 999, 1992]).scalar_at(10) + /// >>> vortex.array([10, 42, 999, 1992]).scalar_at(10) /// Traceback (most recent call last): /// ... /// ValueError: index 10 out of bounds from 0 to 4 @@ -397,7 +397,7 @@ impl PyArray { /// /// Unlike Python, negative indices are not supported: /// - /// >>> vortex.encoding.array([10, 42, 999, 1992]).scalar_at(-2) + /// >>> vortex.array([10, 42, 999, 1992]).scalar_at(-2) /// Traceback (most recent call last): /// ... /// OverflowError: can't convert negative int to unsigned @@ -412,20 +412,20 @@ impl PyArray { /// /// Parameters /// ---------- - /// indices : :class:`vortex.encoding.Array` + /// indices : :class:`~vortex.encoding.Array` /// An array of indices to keep. /// /// Returns /// ------- - /// :class:`vortex.encoding.Array` + /// :class:`~vortex.encoding.Array` /// /// Examples /// -------- /// /// Keep only the first and third elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) - /// >>> indices = vortex.encoding.array([0, 2]) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) + /// >>> indices = vortex.array([0, 2]) /// >>> a.take(indices).to_arrow_array() /// /// [ @@ -435,8 +435,8 @@ impl PyArray { /// /// Permute and repeat the first and second elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) - /// >>> indices = vortex.encoding.array([0, 1, 1, 0]) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) + /// >>> indices = vortex.array([0, 1, 1, 0]) /// >>> a.take(indices).to_arrow_array() /// /// [ @@ -473,14 +473,14 @@ impl PyArray { /// /// Returns /// ------- - /// :class:`vortex.encoding.Array` + /// :class:`~vortex.encoding.Array` /// /// Examples /// -------- /// /// Keep only the second through third elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(1, 3).to_arrow_array() /// /// [ @@ -490,14 +490,14 @@ impl PyArray { /// /// Keep none of the elements: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(3, 3).to_arrow_array() /// /// [] /// /// Unlike Python, it is an error to slice outside the bounds of the array: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(2, 10).to_arrow_array() /// Traceback (most recent call last): /// ... @@ -505,7 +505,7 @@ impl PyArray { /// /// Or to slice with a negative value: /// - /// >>> a = vortex.encoding.array(['a', 'b', 'c', 'd']) + /// >>> a = vortex.array(['a', 'b', 'c', 'd']) /// >>> a.slice(-2, -1).to_arrow_array() /// Traceback (most recent call last): /// ... @@ -533,7 +533,7 @@ impl PyArray { /// /// Uncompressed arrays have straightforward encodings: /// - /// >>> arr = vortex.encoding.array([1, 2, None, 3]) + /// >>> arr = vortex.array([1, 2, None, 3]) /// >>> print(arr.tree_display()) /// root: vortex.primitive(0x03)(i64?, len=4) nbytes=33 B (100.00%) /// metadata: PrimitiveMetadata { validity: Array } diff --git a/pyvortex/src/compress.rs b/pyvortex/src/compress.rs index 52d7a6da8e..845181c6cb 100644 --- a/pyvortex/src/compress.rs +++ b/pyvortex/src/compress.rs @@ -9,7 +9,7 @@ use crate::error::PyVortexError; /// /// Parameters /// ---------- -/// array : :class:`vortex.encoding.Array` +/// array : :class:`~vortex.encoding.Array` /// The array. /// /// Examples @@ -17,23 +17,23 @@ use crate::error::PyVortexError; /// /// Compress a very sparse array of integers: /// -/// >>> a = vortex.encoding.array([42 for _ in range(1000)]) -/// >>> str(vortex.encoding.compress(a)) +/// >>> a = vortex.array([42 for _ in range(1000)]) +/// >>> str(vortex.compress(a)) /// 'vortex.constant(0x09)(i64, len=1000)' /// /// Compress an array of increasing integers: /// -/// >>> a = vortex.encoding.array(list(range(1000))) -/// >>> str(vortex.encoding.compress(a)) +/// >>> a = vortex.array(list(range(1000))) +/// >>> str(vortex.compress(a)) /// 'fastlanes.for(0x17)(i64, len=1000)' /// /// Compress an array of increasing floating-point numbers and a few nulls: /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... float(x) if x % 20 != 0 else None /// ... for x in range(1000) /// ... ]) -/// >>> str(vortex.encoding.compress(a)) +/// >>> str(vortex.compress(a)) /// 'vortex.alp(0x11)(f64?, len=1000)' pub fn compress<'py>(array: &Bound<'py, PyArray>) -> PyResult> { let compressor = SamplingCompressor::default(); diff --git a/pyvortex/src/dtype.rs b/pyvortex/src/dtype.rs index 7a9e49a79a..61f419d0f7 100644 --- a/pyvortex/src/dtype.rs +++ b/pyvortex/src/dtype.rs @@ -119,7 +119,7 @@ pub fn dtype_bool(py: Python<'_>, nullable: bool) -> PyResult> { /// /// Parameters /// ---------- -/// width : one of 8, 16, 32, and 64. +/// width : Literal[8, 16, 32, 64]. /// The bit width determines the span of valid values. If :obj:`None`, 64 is used. /// /// nullable : :class:`bool` @@ -162,7 +162,7 @@ pub fn dtype_int(py: Python<'_>, width: Option, nullable: bool) -> PyResult /// /// Parameters /// ---------- -/// width : one of 8, 16, 32, and 64. +/// width : Literal[8, 16, 32, 64]. /// The bit width determines the span of valid values. If :obj:`None`, 64 is used. /// /// nullable : :class:`bool` @@ -205,7 +205,7 @@ pub fn dtype_uint(py: Python<'_>, width: Option, nullable: bool) -> PyResul /// /// Parameters /// ---------- -/// width : one of 16, 32, and 64. +/// width : Literal[16, 32, 64]. /// The bit width determines the range and precision of the floating-point values. If /// :obj:`None`, 64 is used. /// diff --git a/pyvortex/src/expr.rs b/pyvortex/src/expr.rs index 5b5abe1b21..db9a486bac 100644 --- a/pyvortex/src/expr.rs +++ b/pyvortex/src/expr.rs @@ -13,12 +13,15 @@ use crate::dtype::PyDType; /// An expression describes how to filter rows when reading an array from a file. /// +/// .. seealso:: +/// :func:`.column` +/// /// Examples /// ======== /// /// All the examples read the following file. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': None, 'age': 31}, /// ... {'name': 'Angela', 'age': None}, @@ -209,7 +212,8 @@ impl PyExpr { /// A named column. /// -/// See :class:`.Expr` for more examples. +/// .. seealso:: +/// :class:`.Expr` /// /// Example /// ======= @@ -219,6 +223,8 @@ impl PyExpr { /// >>> name = vortex.expr.column("name") /// >>> filter = name == "Joseph" /// +/// See :class:`.Expr` for more examples. +/// #[pyfunction] pub fn column<'py>(name: &Bound<'py, PyString>) -> PyResult> { let py = name.py(); diff --git a/pyvortex/src/io.rs b/pyvortex/src/io.rs index b7622f732d..5ce85f5895 100644 --- a/pyvortex/src/io.rs +++ b/pyvortex/src/io.rs @@ -8,7 +8,7 @@ use tokio::fs::File; use vortex::Array; use vortex_dtype::field::Field; use vortex_error::VortexResult; -use vortex_sampling_compressor::ALL_COMPRESSORS_CONTEXT; +use vortex_sampling_compressor::{SamplingCompressor, ALL_COMPRESSORS_CONTEXT}; use vortex_serde::layouts::{ LayoutBatchStream, LayoutContext, LayoutDeserializer, LayoutReaderBuilder, LayoutWriter, Projection, RowFilter, @@ -30,7 +30,7 @@ use crate::{PyArray, TOKIO_RUNTIME}; /// /// Read an array with a structured column and nulls at multiple levels and in multiple columns. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': None, 'age': 31}, /// ... {'name': 'Angela', 'age': None}, @@ -114,7 +114,7 @@ use crate::{PyArray, TOKIO_RUNTIME}; /// /// TODO(DK): Top-level nullness does not work. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'name': 'Joseph', 'age': 25}, /// ... {'name': None, 'age': 31}, /// ... {'name': 'Angela', 'age': None}, @@ -208,23 +208,25 @@ pub(crate) async fn async_read( .await } -#[pyfunction] /// Write a vortex struct array to the local filesystem. /// /// Parameters /// ---------- -/// array : :class:`vortex.encoding.Array` +/// array : :class:`~vortex.encoding.Array` /// The array. Must be an array of structures. /// /// f : :class:`str` /// The file path. /// +/// compress : :class:`bool` +/// Compress the array before writing, defaults to ``True``. +/// /// Examples /// -------- /// /// Write the array `a` to the local file `a.vortex`. /// -/// >>> a = vortex.encoding.array([ +/// >>> a = vortex.array([ /// ... {'x': 1}, /// ... {'x': 2}, /// ... {'x': 10}, @@ -233,7 +235,9 @@ pub(crate) async fn async_read( /// ... ]) /// >>> vortex.io.write(a, "a.vortex") /// -pub fn write(array: &Bound<'_, PyArray>, f: &Bound<'_, PyString>) -> PyResult<()> { +#[pyfunction] +#[pyo3(signature = (array, f, *, compress=true))] +pub fn write(array: &Bound<'_, PyArray>, f: &Bound<'_, PyString>, compress: bool) -> PyResult<()> { async fn run(array: &Array, fname: &str) -> VortexResult<()> { let file = File::create(Path::new(fname)).await?; let mut writer = LayoutWriter::new(file); @@ -244,7 +248,15 @@ pub fn write(array: &Bound<'_, PyArray>, f: &Bound<'_, PyString>) -> PyResult<() } let fname = f.to_str()?; // TODO(dk): support file objects - let array = array.borrow().unwrap().clone(); + let mut array = array.borrow().unwrap().clone(); + + if compress { + let compressor = SamplingCompressor::default(); + array = compressor + .compress(&array, None) + .map_err(PyVortexError::new)? + .into_array(); + } TOKIO_RUNTIME .block_on(run(&array, fname)) diff --git a/pyvortex/src/scalar.rs b/pyvortex/src/scalar.rs index abee1bf5dc..dbf9a5ed1e 100644 --- a/pyvortex/src/scalar.rs +++ b/pyvortex/src/scalar.rs @@ -134,7 +134,7 @@ impl PyBufferString { #[pymethods] impl PyBufferString { - /// Copy this buffer string from array memory into a Python str. + /// Copy this buffer string from array memory into a :class:`str`. #[pyo3(signature = (*, recursive = false))] #[allow(unused_variables)] // we want the same Python name across all methods pub fn into_python(self_: PyRef, recursive: bool) -> PyResult { @@ -178,7 +178,7 @@ impl PyVortexList { #[pymethods] impl PyVortexList { - /// Copy the elements of this list from array memory into a list of Python objects. + /// Copy the elements of this list from array memory into a :class:`list`. #[pyo3(signature = (*, recursive = false))] pub fn into_python(self_: PyRef, recursive: bool) -> PyResult { to_python_list(self_.py(), &self_.inner, &self_.dtype, recursive) @@ -236,7 +236,7 @@ impl PyVortexStruct { #[pymethods] impl PyVortexStruct { #[pyo3(signature = (*, recursive = false))] - /// Copy the elements of this list from array memory into a list of Python objects. + /// Copy the elements of this list from array memory into a :class:`dict`. pub fn into_python(self_: PyRef, recursive: bool) -> PyResult { to_python_dict(self_.py(), &self_.inner, &self_.dtype, recursive) } diff --git a/pyvortex/test/test_dataset.py b/pyvortex/test/test_dataset.py index 62a9a6479e..f02d4392e1 100644 --- a/pyvortex/test/test_dataset.py +++ b/pyvortex/test/test_dataset.py @@ -24,7 +24,7 @@ def ds(tmpdir_factory) -> vortex.dataset.VortexDataset: a = pa.array([record(x) for x in range(1_000_000)]) arr = vortex.encoding.compress(vortex.array(a)) vortex.io.write(arr, "/tmp/foo.vortex") - return vortex.dataset.VortexDataset("/tmp/foo.vortex") + return vortex.dataset.dataset("/tmp/foo.vortex") def test_schema(ds): diff --git a/requirements-dev.lock b/requirements-dev.lock index 5a9bc870cc..6df3b41447 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,6 @@ numpy==1.26.4 # via xarray packaging==24.0 # via matplotlib - # via pydata-sphinx-theme # via pytest # via sphinx # via xarray @@ -103,7 +102,7 @@ py-cpuinfo==9.0.0 # via pytest-benchmark pyarrow==17.0.0 # via vortex-array -pydata-sphinx-theme==0.15.4 +pydata-sphinx-theme==0.16.0 pygments==2.17.2 # via accessible-pygments # via ipython @@ -133,6 +132,8 @@ soupsieve==2.6 # via beautifulsoup4 sphinx==8.0.2 # via pydata-sphinx-theme + # via sphinx-design +sphinx-design==0.6.1 sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 diff --git a/requirements.lock b/requirements.lock index 88d7d0993d..d8458d2455 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,7 +55,6 @@ numpy==2.1.2 # via xarray packaging==24.1 # via matplotlib - # via pydata-sphinx-theme # via sphinx # via xarray pandas==2.2.3 @@ -70,7 +69,7 @@ protobuf==5.28.2 # via substrait pyarrow==17.0.0 # via vortex-array -pydata-sphinx-theme==0.15.4 +pydata-sphinx-theme==0.16.0 pygments==2.18.0 # via accessible-pygments # via pydata-sphinx-theme @@ -93,6 +92,8 @@ soupsieve==2.6 # via beautifulsoup4 sphinx==8.1.3 # via pydata-sphinx-theme + # via sphinx-design +sphinx-design==0.6.1 sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 From 26d7e01b2939c8634936296140b7656f8c41a503 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Thu, 24 Oct 2024 18:20:35 -0400 Subject: [PATCH 2/9] less marketing more serious --- docs/index.rst | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index b327422fc8..8a68858caa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,32 +9,28 @@ Wide, Fast & Compact. Pick Three. .. grid:: 1 1 2 2 :gutter: 4 4 4 4 - .. grid-item-card:: Row groups? Your choice. - :link: file-format--layouts - :link-type: ref + .. grid-item-card:: The File Format + :link: file_format + :link-type: doc - Bring your wide schemas, images, and videos. + Currently just a schematic. Specification forthcoming. - .. grid-item-card:: All your favorite query engines. - :link: query-engine-integration - :link-type: ref + .. grid-item-card:: The Rust API + :link: https://spiraldb.github.io/vortex/docs2/rust/doc/vortex - Query pushdown in Pandas, Polars, DuckDB, & chDB. + The primary interface to the Vortex toolkit. - .. grid-item-card:: 200x lower latency random reads. + .. grid-item-card:: Quickstart + :link: quickstart + :link-type: doc - Block compression is for chumps. + For end-users looking to read and write Vortex files. - .. grid-item-card:: Zero copy reads. + .. grid-item-card:: The Benchmarks + :link: https://bench.vortex.dev/ - It's called a processor, not a Xerox. + Random access, throughput, and TPC-H. - .. grid-item-card:: Batteries-included. - :columns: 12 12 12 12 - :link: included-codecs - :link-type: ref - - Cutting-edge codecs: FSST, ALP, FastLanes, and more. Vortex is a fast, extensible, lightweight-compressed, and random-access columnar file format as well as a library for working with compressed Apache Arrow arrays in-memory, on-disk, and From c94686d28f60c3ccba603151c09b38aede1365f1 Mon Sep 17 00:00:00 2001 From: Dan King Date: Fri, 25 Oct 2024 09:42:45 -0400 Subject: [PATCH 3/9] work around line length lint --- pyvortex/python/vortex/encoding.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyvortex/python/vortex/encoding.py b/pyvortex/python/vortex/encoding.py index d36401af64..75eeb5655c 100644 --- a/pyvortex/python/vortex/encoding.py +++ b/pyvortex/python/vortex/encoding.py @@ -284,10 +284,9 @@ def _Array_to_pylist(self: _encoding.Array) -> list[Any]: ... {'name': 'Joseph', 'age': 25}, ... {'name': 'Narendra', 'age': 31}, ... {'name': 'Angela', 'age': 33}, - ... {'name': 'Mikhail', 'age': 57}, ... ]) >>> array.to_pylist() - [{'age': 25, 'name': 'Joseph'}, {'age': 31, 'name': 'Narendra'}, {'age': 33, 'name': 'Angela'}, {'age': 57, 'name': 'Mikhail'}] + [{'age': 25, 'name': 'Joseph'}, {'age': 31, 'name': 'Narendra'}, {'age': 33, 'name': 'Angela'}] """ return self.to_arrow_table().to_pylist() From 80e99ef4b52270c39a59117059392cd85419e554 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Fri, 25 Oct 2024 17:55:13 -0400 Subject: [PATCH 4/9] missing vortex file --- docs/_static/example.vortex | Bin 0 -> 76904 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/_static/example.vortex diff --git a/docs/_static/example.vortex b/docs/_static/example.vortex new file mode 100644 index 0000000000000000000000000000000000000000..796654f2a778615b055e9478de78fd7d12515674 GIT binary patch literal 76904 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 literal 0 HcmV?d00001 From bdf19c3aa11e08c80ef612f7f582a55c5b4b5ed9 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Fri, 25 Oct 2024 17:57:39 -0400 Subject: [PATCH 5/9] wip --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0c4018327c..1909ea8c60 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -46,9 +46,9 @@ Use :func:`~vortex.encoding.compress` to compress the Vortex array and check the >>> cvtx = vortex.compress(vtx) >>> cvtx.nbytes - 16174 + 13970 >>> cvtx.nbytes / vtx.nbytes - 0.11468969820739733 + 0.099... Vortex uses nearly ten times fewer bytes than Arrow. Fewer bytes means more of your data fits in cache and RAM. @@ -70,7 +70,7 @@ similar to or smaller than Parquet. >>> from os.path import getsize >>> getsize("example.vortex") / getsize("_static/example.parquet") - 2.17... + 2.16... Read ^^^^ From fe35b43da323119d785649a375f4d8246afbfaa1 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Mon, 28 Oct 2024 15:57:30 -0400 Subject: [PATCH 6/9] further relax precision on ratio --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1909ea8c60..890dd6aea4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -70,7 +70,7 @@ similar to or smaller than Parquet. >>> from os.path import getsize >>> getsize("example.vortex") / getsize("_static/example.parquet") - 2.16... + 2.1... Read ^^^^ From 98cf05f7e20070b8b87cc02472bbf49c7cef4f46 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Mon, 28 Oct 2024 17:36:30 -0400 Subject: [PATCH 7/9] address comments --- docs/file_format.rst | 11 +++++++---- docs/index.rst | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/file_format.rst b/docs/file_format.rst index 9f3f6a2ccf..322bcd503b 100644 --- a/docs/file_format.rst +++ b/docs/file_format.rst @@ -32,10 +32,13 @@ size. A chunked layout permits reader to push-down row filters based on statisti later. Note that, if the laid out array is a struct array, each column uses the same chunk size. This is equivalent to Parquet's row groups. -The layout: chunked of struct of chunked of flat, is essentially a Parquet layout with row groups in -which each column's values are contiguously stored in pages. The layout: struct of chunked of flat -eliminates row groups, retaining only pages. The layout struct of flat does not permit any row -filter push-down because each array is, to the layout, an opaque sequence of bytes. +A few examples of concrete layouts: + +1. Chunked of struct of chunked of flat: essentially a Parquet layout with row groups in which each + column's values are contiguously stored in pages. +2. Struct of chunked of flat: eliminates row groups, retaining only pages. +3. Struct of flat: prevents row filter push-down because each array is, to the layout, an opaque + sequence of bytes. The chunked layout stores, per chunk, metadata necessary for effective row filtering such as sortedness, constancy, the minimum value, the maximum value, and the number of null rows. Readers diff --git a/docs/index.rst b/docs/index.rst index 8a68858caa..2a2d9e9232 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,11 +32,19 @@ Wide, Fast & Compact. Pick Three. Random access, throughput, and TPC-H. -Vortex is a fast, extensible, lightweight-compressed, and random-access columnar file format as well -as a library for working with compressed Apache Arrow arrays in-memory, on-disk, and -over-the-wire. Vortex aspires to succeed Apache Parquet by delivering two orders of magnitude faster -random-access and faster scans without sacrificing compression ratio nor write throughput. Its -features include: +Vortex is a fast & extensible columnar file format that is based around state-of-the-art research +from the database community. It is built around cascading compression with lightweight encodings (no +block compression), allowing for both efficient random access and extremely fast decompression. + +Vortex also includes an accompanying in-memory format for these (recursively) compressed arrays, +that is zero-copy compatible with Apache Arrow in uncompressed form. Taken together, the Vortex +library is a useful toolkit with compressed Arrow data in-memory, on-disk, & over-the-wire. + +Vortex aspires to succeed Apache Parquet by pushing the Pareto frontier outwards: 1-2x faster +writes, 2-10x faster scans, and 100-200x faster random access reads, while preserving the same +approximate compression ratio as Parquet v2 with zstd. + +Its features include: - A zero-copy data layout for disk, memory, and the wire. - Kernels for computing on, filtering, slicing, indexing, and projecting compressed arrays. @@ -46,7 +54,7 @@ features include: - Support for, but no requirement for, row groups. - A read sub-system supporting filter and projection pushdown. -Spiral's flexible layout empowers writers to choose the right layout for their setting: fast writes, +Vortex's flexible layout empowers writers to choose the right layout for their setting: fast writes, fast reads, small files, few columns, many columns, over-sized columns, etc. Documentation From a8982f8287298ab3129e725645cf787ca110da10 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Mon, 28 Oct 2024 17:49:41 -0400 Subject: [PATCH 8/9] merge cruft --- docs/guide.rst | 4 ++-- docs/quickstart.rst | 4 ++-- pyvortex/python/vortex/dataset.py | 8 ++++++-- uv.lock | 16 +++++++++++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/guide.rst b/docs/guide.rst index 9b20e188c0..068d12e708 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -135,7 +135,7 @@ Polars ^^^^^^ >>> import polars as pl - >>> ds = vortex.dataset.dataset( + >>> ds = vortex.dataset.from_path( ... '_static/example.vortex' ... ) >>> lf = pl.scan_pyarrow_dataset(ds) @@ -157,7 +157,7 @@ DuckDB ^^^^^^ >>> import duckdb - >>> ds = vortex.dataset.dataset( + >>> ds = vortex.dataset.from_path( ... '_static/example.vortex' ... ) >>> duckdb.sql('select ds.tip_amount, ds.fare_amount from ds limit 3').show() diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 890dd6aea4..417741e665 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -60,7 +60,7 @@ Use :func:`~vortex.io.write` to write the Vortex array to disk: .. doctest:: - >>> vortex.io.write(cvtx, "example.vortex") + >>> vortex.io.write_path(cvtx, "example.vortex") Small Vortex files (this one is just 71KiB) currently have substantial overhead relative to their size. This will be addressed shortly. On files with at least tens of megabytes of data, Vortex is @@ -79,7 +79,7 @@ Use :func:`~vortex.io.read` to read the Vortex array from disk: .. doctest:: - >>> cvtx = vortex.io.read("example.vortex") + >>> cvtx = vortex.io.read_path("example.vortex") .. _rust-quickstart: diff --git a/pyvortex/python/vortex/dataset.py b/pyvortex/python/vortex/dataset.py index 7d96a5d2d5..7f9d8d5d3b 100644 --- a/pyvortex/python/vortex/dataset.py +++ b/pyvortex/python/vortex/dataset.py @@ -399,8 +399,12 @@ def to_table( return self._dataset.to_array(columns=columns, batch_size=batch_size, row_filter=filter).to_arrow_table() -def dataset(fname: str) -> VortexDataset: - return VortexDataset(fname) +def from_path(path: str) -> VortexDataset: + return VortexDataset(_lib_dataset.dataset_from_path(path)) + + +def from_url(url: str) -> VortexDataset: + return VortexDataset(_lib_dataset.dataset_from_url(url)) class VortexScanner(pa.dataset.Scanner): diff --git a/uv.lock b/uv.lock index 519b3c0a95..0e5b0edd34 100644 --- a/uv.lock +++ b/uv.lock @@ -242,13 +242,15 @@ dependencies = [ { name = "pydata-sphinx-theme" }, { name = "pyvortex" }, { name = "sphinx" }, + { name = "sphinx-design" }, ] [package.metadata] requires-dist = [ - { name = "pydata-sphinx-theme", specifier = ">=0.15.4" }, + { name = "pydata-sphinx-theme", specifier = ">=0.16.0" }, { name = "pyvortex" }, { name = "sphinx", specifier = ">=8.0.2" }, + { name = "sphinx-design", specifier = ">=0.6.1" }, ] [[package]] @@ -1189,6 +1191,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, ] +[[package]] +name = "sphinx-design" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338 }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" From 5c78a6e104633e0e7cfae0bedfbbbb09665970c2 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Tue, 29 Oct 2024 10:30:30 -0400 Subject: [PATCH 9/9] more merge cruft --- docs/api/io.rst | 5 +++-- docs/quickstart.rst | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/api/io.rst b/docs/api/io.rst index 42c7e0de94..1dee8dea5d 100644 --- a/docs/api/io.rst +++ b/docs/api/io.rst @@ -7,8 +7,9 @@ HTTP, S3, Google Cloud Storage, and Azure Blob Storage. .. autosummary:: :nosignatures: - ~vortex.io.read - ~vortex.io.write + ~vortex.io.read_path + ~vortex.io.read_url + ~vortex.io.write_path .. raw:: html diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 417741e665..65a71cb7c3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -56,7 +56,7 @@ cache and RAM. Write ^^^^^ -Use :func:`~vortex.io.write` to write the Vortex array to disk: +Use :func:`~vortex.io.write_path` to write the Vortex array to disk: .. doctest:: @@ -75,7 +75,7 @@ similar to or smaller than Parquet. Read ^^^^ -Use :func:`~vortex.io.read` to read the Vortex array from disk: +Use :func:`~vortex.io.read_path` to read the Vortex array from disk: .. doctest::