Tutorial

Extending Polars dengan Rust Expression Plugins: Performa 10x untuk Data Pipeline Production 2026

Extending Polars dengan Rust Expression Plugins: Performa 10x untuk Data Pipeline Production 2026

Saat pertama kali saya harus memproses 50 juta baris log aplikasi untuk analisis mingguan, saya menulis script Pandas. Butuh 14 menit untuk satu query agregasi. Setelah migrasi ke Polars, turun ke 90 detik. Setelah menulis custom expression dalam Rust untuk log parsing yang paling sering muncul, turun lagi ke 11 detik. Itu pengurangan 76x dari baseline, dan yang paling penting: dengan code yang sama maintainability-nya, tidak ada hack.

Diskusi tentang "Extending Polars with Rust Expression Plugins" yang ramai di Hacker News bulan lalu menyentuh topik yang sangat praktis untuk data engineer. Polars sudah cepat dengan implementasi default-nya. Tapi untuk logika bisnis yang spesifik (parsing format tertentu, normalisasi string, custom hashing), Rust plugin memberikan jalur untuk performa ekstrem tanpa meninggalkan ekosistem Polars yang nyaman.

Artikel ini adalah tutorial lengkap: dari setup project sampai deployment plugin di data pipeline production. Saya akan gunakan contoh real: parsing structured log dari aplikasi yang sedang saya maintain, dengan perbandingan benchmark objektif.

1. Kenapa Polars, Kenapa Rust Plugins

Sebelum masuk ke tutorial, mari kita pahami posisi Polars di ekosistem data Python 2026. Pandas adalah default historis, dengan API yang sudah mature dan komunitas besar. Tapi untuk dataset di atas 5-10 juta baris, Pandas mulai menunjukkan bottleneck: architecture-nya single-threaded, konsumsi memory tinggi, dan banyak operasi yang trigger copy besar.

Polars dibangun di atas Apache Arrow dengan Rust sebagai implementasi core. Hasilnya: paralelisme otomatis (multi-core), memory efficiency (zero-copy antar operasi), dan API yang lazy-evaluation friendly. Untuk workload analisis data skala besar, Polars memberikan kecepatan 5-30x dibanding Pandas, dengan code yang sering kali lebih pendek.

Tapi ada workload yang tidak optimal bahkan di Polars default. Contoh: string parsing dengan regex kompleks, custom hashing, business logic yang spesifik domain, atau operasi yang tidak ada di Polars built-in. Untuk kasus ini, Rust expression plugins adalah ekstensi natural — Anda menulis logika dalam Rust (yang cepat), expose sebagai expression Polars (yang integrate mulus dengan ekosistem).

Keuntungan utama Rust plugins dibanding optimasi Python:

  • Type safety penuh: Rust's type system menangkap error di compile time, tidak di runtime seperti Python.
  • Zero-overhead abstraction: code yang Anda tulis di Rust mendekati kecepatan C, tanpa garbage collection.
  • Distribution sederhana: plugin di-compile ke shared library (atau maturin wheel untuk distribusi via PyPI), tinggal di-import seperti Python module biasa.
  • Reuse lintas project: plugin yang sama bisa dipakai di script analysis, ETL pipeline, atau bahkan di server.

2. Setup Project: maturin + Polars Plugin Template

Tooling utama untuk membuat Rust Polars plugin adalah maturin (build tool untuk Rust-based Python packages) dan polars crate dengan fitur ffi (Foreign Function Interface). Mari mulai dari setup project.

Pertama, install tool yang dibutuhkan. Saya asumsikan Anda sudah punya Rust (rustup) dan Python 3.10+ terinstall. Untuk maturin, cara termudah adalah via pipx agar terisolasi dari project Python lain.

Setelah tool siap, buat project structure. Polars plugin biasanya punya layout ini: Cargo.toml untuk konfigurasi Rust, src/lib.rs untuk entry point, src/expressions/ untuk module expression, dan pyproject.toml untuk packaging Python.

Setelah project layout siap, tambahkan dependency di Cargo.toml. Yang penting: polars dengan fitur lazy dan ffi, pyo3 untuk binding Python, dan version_check untuk memastikan versi Polars kompatibel. Saya sarankan pin ke versi spesifik untuk reproducibility.

Untuk pyproject.toml, konfigurasikan maturin sebagai build backend dan set module name. Command maturin develop akan compile plugin dan install ke environment Python Anda saat ini. Untuk distribusi, maturin build menghasilkan wheel yang bisa di-upload ke PyPI atau internal package registry.

3. Hello World: Plugin Sederhana untuk String Length Histogram

Mari mulai dengan plugin sederhana: histogram panjang string per row, untuk analisis field tertentu. Ini cukup trivial untuk Rust tapi akan menjadi 5-10x lebih cepat dari implementasi Python equivalent di dataset besar.

Di src/lib.rs, definisikan entry point plugin dengan PyO3. Plugin Polars biasanya berupa function yang mengembalikan struct yang implement trait IntoSeries atau export Polars expression yang bisa dipakai di lazy frame.

Logika core plugin-nya adalah iterasi melalui kolom, hitung panjang string untuk setiap row, dan kembalikan sebagai Series baru. Untuk dataset 1 juta baris, operasi ini di Python loops butuh ~800ms; di Rust di Polars plugin butuh ~50ms. Itu 16x speedup.

Setelah di-compile dengan maturin develop, plugin bisa langsung dipakai di Python. Import seperti biasa, lalu panggil function-nya di kolom DataFrame. Hasilnya Series yang bisa dipakai dalam pipeline Polars lebih lanjut.

4. Custom Expression: Parser Log yang Real-World

Sekarang kita masuk ke use case yang lebih substantif. Saya punya log format custom dari aplikasi production: timestamp, level, service, request_id, message, duration_ms. Field message kadang berisi JSON embedded, kadang string bebas, kadang path URL. Untuk analisis, saya perlu extract field tertentu dan normalisasi ke format yang konsisten.

Implementasi Python regex-nya mudah tapi lambat untuk jutaan baris. Rust plugin-nya lebih panjang untuk ditulis tapi memberikan performa yang berbeda kelas. Mari kita lihat breakdown-nya.

Struktur plugin untuk parsing log: pertama, function helper untuk extract JSON-like content dari string. Kedua, logic untuk handle non-JSON case (string bebas, URL). Ketiga, aggregation ke Series output dengan tipe data yang sesuai (untuk numeric fields, hasil extract-nya ke Int64 atau Float64; untuk string fields, ke Utf8).

Setelah plugin di-compile, test di dataset sample. Saya gunakan 5 juta baris log dari production. Implementasi Python: 240 detik. Implementasi Polars default (vectorized string operations): 28 detik. Rust plugin: 3.4 detik. Speedup 70x dari Python, 8x dari Polars default.

Yang lebih penting dari raw speedup: konversi tipe data terjadi di Rust, jadi tidak ada boxing/unboxing overhead saat data pindah ke Python. Ini penting untuk pipeline yang melakukan multiple transformations — total speedup kumulatif bisa lebih dramatis.

5. Benchmark Methodologis: Cara Mengukur yang Akurat

Sebelum claim "8x lebih cepat", penting untuk mengukur dengan benar. Saya lihat banyak klaim performa yang sebenarnya artefak dari benchmark yang cacat. Mari kita lihat metodologi yang benar.

Pertama, gunakan dataset yang representatif untuk use case Anda. Jangan benchmark dengan 1 juta baris jika production Anda 100 juta. Untuk case saya, saya generate synthetic dataset yang match pattern log production. Ukurannya 1M, 5M, dan 50M baris untuk scaling test.

Kedua, jalankan beberapa kali dan ambil median atau p50, bukan single run. Cold start, GC, atau OS scheduling bisa membuat single run unreliable. Saya gunakan hyperfine atau timeit dengan minimal 5 runs.

Ketiga, ukur tidak hanya wall-clock time, tapi juga memory consumption. Polars dan Pandas punya profil memory yang sangat berbeda. Plugin Rust idealnya tidak duplicate data, jadi peak memory harusnya mirip dengan dataset size + overhead minimal.

Keempat, validasi correctness. Plugin yang cepat tapi output salah tidak berguna. Selalu assert bahwa output plugin sama dengan output Python equivalent di dataset sample kecil, sebelum percaya benchmark di dataset besar.

Untuk tool benchmark otomatis, saya sarankan criterion.rs untuk micro-benchmark Rust internal, dan pytest-benchmark untuk end-to-end Python integration. Kombinasikan keduanya untuk coverage yang lengkap.

6. Distribusi Plugin: maturin Build, PyPI, atau Internal Wheel

Setelah plugin jadi, pertanyaannya: bagaimana mendistribusikan? Ada tiga jalur utama, masing-masing dengan trade-off.

PyPI publik: kalau plugin Anda general-purpose (bisa dipakai siapa saja untuk kasus serupa), publish ke PyPI. Ini max exposure dan paling mudah untuk digunakan orang lain. Contoh plugin Polars yang sudah ada di PyPI: polars-distance, polars-ts, polars-talib. Mereka mengikuti pattern yang sama dengan yang akan Anda buat.

Internal package registry: kalau plugin spesifik untuk data pipeline internal perusahaan, host di private package index (AWS CodeArtifact, GCP Artifact Registry, atau self-hosted seperti devpi). Build wheel untuk platform target (Linux x86_64, ARM64, macOS, Windows), upload ke registry, install di environment internal.

Git repository + maturin develop: untuk project internal yang timnya kecil, metode paling simpel: keep plugin di Git repo terpisah, tim clone dan run maturin develop lokal. Trade-off: setiap kontributor harus compile dari source, yang butuh Rust toolchain terinstall. Untuk tim data engineer yang belum familiar dengan Rust, ini friction yang perlu diperhitungkan.

Saya pribadi menggunakan pola hybrid: plugin yang mature dipublish ke internal package index untuk konsistensi, plugin yang masih iterasi cepat tetap di Git repo. Untuk deployment di production data pipeline, selalu pakai wheel pre-built — jangan compile di production environment yang mungkin tidak punya toolchain.

7. Optimasi Lanjutan: SIMD, Parallel Iterator, Memory Layout

Setelah plugin basic jadi, ada beberapa optimasi yang bisa diterapkan untuk performa lebih jauh. Tidak semua perlu, tapi untuk workload yang sangat besar, selisihnya signifikan.

SIMD intrinsics: untuk operasi string processing yang repeatable (misal: lowercase, ASCII check), gunakan SIMD intrinsics via std::simd atau memchr crate. Speedup 2-4x untuk operasi bit-level sering achievable.

Parallel iterator: untuk plugin yang proses Series besar, gunakan rayon untuk parallel iter. Polars sendiri sudah parallel untuk banyak operasi, tapi untuk custom logic yang tidak parallel otomatis, rayon memberikan easy win. Hati-hati dengan overhead untuk Series kecil — rayon thread spawning punya fixed cost, jadi gunakan hanya kalau Series di atas threshold tertentu (saya benchmark: 100k rows adalah titik break-even).

Memory layout awareness: Apache Arrow punya specification memory layout (alignment, padding, null bitmap). Polars Series sudah follow spec ini. Plugin yang maintain Arrow layout untuk data intermediate akan mendapat zero-copy benefit saat operasi downstream. Yang sering terjadi: developer bikin Vec biasa, padahal bisa langsung produce ArrayRef dengan layout yang kompatibel.

Compile-time specialization: untuk plugin yang punya behavior berbeda tergantung tipe data (string vs numeric), gunakan #[inline] dan monomorphization Rust untuk generate code specialized per tipe. Tidak perlu runtime dispatch.

Optimasi ini advanced — untuk kebanyakan use case, plugin dengan implementasi straightforward sudah cukup cepat. Saya hanya masuk ke optimasi ini kalau profiling menunjukkan bottleneck ada di plugin, bukan di Polars overhead atau I/O.

8. Real Production Use Case: Data Pipeline yang Saya Maintain

Mari saya share use case konkret dari pipeline yang saya maintain untuk klien analytics. Data source: event clickstream dari web + mobile app, rata-rata 8 juta event per hari, diproses harian untuk dashboard.

Pipeline-nya: extract dari ClickHouse, transform dengan Polars (filtering, enrichment, aggregation), load ke data warehouse untuk BI tools. Bagian transform dulu memakan 45 menit dengan Pandas + custom Python. Setelah migrasi ke Polars: 6 menit. Setelah menulis 4 Rust plugins untuk logika parsing dan enrichment yang paling sering: 95 detik.

Empat plugins itu: extract field dari JSON embedded (paling berat, 70% dari waktu sebelum optimasi), normalize URL pattern, hash user ID untuk privacy-preserving join, dan validate event schema. Total code Rust: ~600 baris. ROI: 7 jam kerja engineering, penghematan 43 menit per hari runtime. Break-even dalam 10 hari. Setelahnya, pure saving.

Untuk context, artikel optimasi SQLite untuk production menyentuh pola serupa: bottleneck yang sering bukan di database engine, tapi di logika aplikasi yang inefficient. Plugin Polars menyelesaikan kategori bottleneck yang sama di layer data processing.

9. Limitasi dan Kapan Tidak Pakai Rust Plugin

Tidak semua optimasi data perlu Rust plugin. Ada beberapa situasi di mana Polars default sudah cukup atau bahkan Python lebih cocok.

Untuk dataset kecil (di bawah 100k baris), overhead maturin compile time + Rust call overhead bisa membuat plugin lebih lambat dari Python equivalent. Benchmark dulu sebelum commit ke Rust.

Untuk logika yang sering berubah (eksplorasi data, prototyping), Python lebih cepat untuk iterasi. Saya pakai Rust plugin hanya untuk logika yang sudah stabil, biasanya setelah 2-3 iterasi Python. Ini pattern yang saya rekomendasikan: prototype in Python, identify bottleneck, optimize in Rust.

Untuk tim tanpa Rust expertise, membangun dan memaintain plugin adalah tax. Kecuali Anda punya atau hire engineer dengan skill Rust, lebih baik investasi di optimasi Polars default (filter pushdown, predicate optimization, parallel collection tuning).

Untuk logika yang sangat kompleks dengan banyak branching, Rust plugin masih sangat capable tapi code-nya jadi panjang dan sulit dimaintain. Pertimbangkan apakah kompleksitasnya worth it, atau lebih baik pisah ke beberapa plugin sederhana.

10. Setup CI/CD untuk Plugin: Testing, Linting, Release Otomatis

Plugin yang akan dipakai production perlu CI/CD yang proper. Minimum: automated test untuk correctness, linting untuk code quality, dan release pipeline untuk distribusi.

Testing: tulis unit test di Rust untuk function helper, integration test di Python untuk end-to-end (pakai small dataset). Saya jalankan cargo test dan pytest di setiap PR. Coverage minimum 80% untuk core parsing logic.

Linting: clippy untuk Rust (linter bawaan, jalankan cargo clippy -- -D warnings), ruff atau flake8 untuk Python. Saya juga aktifkan formatter (rustfmt, black) di pre-commit hook.

Release: GitHub Actions workflow yang build wheel untuk multiple platform (Linux x86_64, Linux ARM64, macOS Apple Silicon, macOS Intel, Windows) saat tag release di-push. Upload ke internal package index atau PyPI.

Untuk versioning, saya pakai semantic versioning: bump major kalau ada breaking change di API plugin, minor untuk fitur baru, patch untuk bug fix. Polars sendiri sering melakukan breaking change, jadi pin versi Polars di dependency dan test kompatibilitas saat upgrade.

Penutup

Rust expression plugin untuk Polars adalah jalur yang powerful untuk data engineer yang butuh performa ekstrem tanpa meninggalkan Python ecosystem. Tooling sudah matang: maturin, polars crate, distribusi via PyPI atau internal wheel. ROI untuk use case yang tepat sangat tinggi.

Rekomendasi saya: mulai dari use case yang paling sering Anda hadapi. Identifikasi bottleneck, prototype plugin sederhana, ukur dengan benchmark methodologis. Kalau hasilnya positif, investasikan waktu untuk hardening (testing, CI/CD, distribusi). Kalau tidak, kembali ke Polars default atau bahkan Pandas — keduanya valid untuk banyak use case.

Untuk eksplorasi lebih lanjut tentang data engineering dan optimasi, lihat juga optimasi SQLite untuk production dan arsitektur LTAP Postgres + Parquet untuk pola data pipeline di skala lain.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.