diff --git a/crates/bin/ampctl/src/cmd/manifest/generate.rs b/crates/bin/ampctl/src/cmd/manifest/generate.rs index 6ae90a070..aeae12b9c 100644 --- a/crates/bin/ampctl/src/cmd/manifest/generate.rs +++ b/crates/bin/ampctl/src/cmd/manifest/generate.rs @@ -174,6 +174,7 @@ fn generate_evm_rpc_manifest( let manifest_table = ManifestTable { schema, network: network.clone(), + bloom_filter_columns: Vec::new(), }; (table.name().to_string(), manifest_table) }) @@ -204,6 +205,7 @@ fn generate_solana_manifest( let manifest_table = ManifestTable { schema, network: network.clone(), + bloom_filter_columns: Vec::new(), }; (table.name().to_string(), manifest_table) }) @@ -234,6 +236,7 @@ fn generate_firehose_manifest( let manifest_table = ManifestTable { schema, network: network.clone(), + bloom_filter_columns: Vec::new(), }; (table.name().to_string(), manifest_table) }) @@ -264,6 +267,7 @@ fn generate_tempo_manifest( let manifest_table = ManifestTable { schema, network: network.clone(), + bloom_filter_columns: Vec::new(), }; (table.name().to_string(), manifest_table) }) diff --git a/crates/config/src/worker_core.rs b/crates/config/src/worker_core.rs index d7c030fd6..9566a6a3a 100644 --- a/crates/config/src/worker_core.rs +++ b/crates/config/src/worker_core.rs @@ -16,9 +16,6 @@ pub struct ParquetConfig { /// Compression algorithm (default: `zstd(1)`). #[serde(default)] pub compression: Compression, - /// Enable Parquet bloom filters (default: false). - #[serde(default)] - pub bloom_filters: bool, /// Parquet metadata cache size in MB (default: 1024). #[serde(default = "default_cache_size_mb")] pub cache_size_mb: u64, @@ -48,7 +45,6 @@ impl Default for ParquetConfig { fn default() -> Self { Self { compression: Compression::default(), - bloom_filters: false, cache_size_mb: default_cache_size_mb(), max_row_group_mb: default_max_row_group_mb(), target_size: SizeLimitConfig::default(), @@ -71,7 +67,6 @@ impl From<&ParquetConfig> for amp_worker_core::ParquetConfig { fn from(config: &ParquetConfig) -> Self { Self { compression: (&config.compression).into(), - bloom_filters: config.bloom_filters, cache_size_mb: config.cache_size_mb, max_row_group_mb: config.max_row_group_mb, target_size: (&config.target_size).into(), diff --git a/crates/core/datasets-common/src/dataset.rs b/crates/core/datasets-common/src/dataset.rs index 9a74c9353..99a278ee0 100644 --- a/crates/core/datasets-common/src/dataset.rs +++ b/crates/core/datasets-common/src/dataset.rs @@ -18,7 +18,7 @@ use downcast_rs::{DowncastSync, impl_downcast}; use crate::{ block_num::BlockNum, dataset_kind_str::DatasetKindStr, hash_reference::HashReference, - network_id::NetworkId, table_name::TableName, + manifest::BloomFilterColumnConfig, network_id::NetworkId, table_name::TableName, }; /// Core trait representing a dataset definition. @@ -95,6 +95,13 @@ pub trait Table: DowncastSync + std::fmt::Debug { /// Returns column names by which this table is naturally sorted. Always includes `_block_num`. fn sorted_by(&self) -> &BTreeSet; + + /// Returns the bloom filter column configurations for this table. + /// + /// Empty by default — tables without bloom filter config get no bloom filters. + fn bloom_filter_columns(&self) -> &[BloomFilterColumnConfig] { + &[] + } } // Implement downcasting for `Table`. diff --git a/crates/core/datasets-common/src/manifest.rs b/crates/core/datasets-common/src/manifest.rs index 81c6e11c0..5f2019e21 100644 --- a/crates/core/datasets-common/src/manifest.rs +++ b/crates/core/datasets-common/src/manifest.rs @@ -61,6 +61,25 @@ impl Default for Schema { } } +/// Configuration for a bloom filter on a specific column. +/// +/// Bloom filters enable efficient row-group pruning during queries by quickly +/// determining whether a row group could contain a specific value. The `ndv` +/// (number of distinct values) parameter tunes the filter's false-positive rate. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct BloomFilterColumnConfig { + /// Column name to apply the bloom filter to. + pub column: String, + /// Estimated number of distinct values for this column (default: 10,000). + #[serde(default = "default_bloom_filter_ndv")] + pub ndv: u64, +} + +fn default_bloom_filter_ndv() -> u64 { + 10_000 +} + /// Apache Arrow data _new-type_ wrapper with JSON schema support. /// /// This wrapper provides serialization and JSON schema generation capabilities diff --git a/crates/core/datasets-raw/src/dataset.rs b/crates/core/datasets-raw/src/dataset.rs index 6e63d59f5..ad5fad750 100644 --- a/crates/core/datasets-raw/src/dataset.rs +++ b/crates/core/datasets-raw/src/dataset.rs @@ -1,6 +1,9 @@ //! Concrete raw dataset and table types. -use std::{collections::BTreeSet, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; use arrow::datatypes::SchemaRef; use datasets_common::{ @@ -8,6 +11,7 @@ use datasets_common::{ dataset::Table as TableTrait, dataset_kind_str::DatasetKindStr, hash_reference::HashReference, + manifest::BloomFilterColumnConfig, network_id::NetworkId, table_name::TableName, }; @@ -37,7 +41,8 @@ impl Dataset { reference: HashReference, kind: DatasetKindStr, network: NetworkId, - tables: Vec, + mut tables: Vec
, + manifest_tables: &BTreeMap, start_block: Option, finalized_blocks_only: bool, ) -> Self { @@ -50,6 +55,13 @@ impl Dataset { "all tables must belong to the same network as the dataset" ); + // Apply per-table bloom filter config from the manifest. + for table in &mut tables { + if let Some(mt) = manifest_tables.get(table.name().as_str()) { + table.set_bloom_filter_columns(mt.bloom_filter_columns.clone()); + } + } + let tables: Vec> = tables .into_iter() .map(|t| Arc::new(t) as Arc) @@ -111,6 +123,7 @@ pub struct Table { schema: SchemaRef, network: NetworkId, sorted_by: BTreeSet, + bloom_filter_columns: Vec, } impl Table { @@ -128,6 +141,7 @@ impl Table { schema, network, sorted_by, + bloom_filter_columns: Vec::new(), } } @@ -135,6 +149,11 @@ impl Table { pub fn network_ref(&self) -> &NetworkId { &self.network } + + /// Sets the bloom filter column configurations for this table. + pub fn set_bloom_filter_columns(&mut self, columns: Vec) { + self.bloom_filter_columns = columns; + } } impl TableTrait for Table { @@ -153,4 +172,8 @@ impl TableTrait for Table { fn sorted_by(&self) -> &BTreeSet { &self.sorted_by } + + fn bloom_filter_columns(&self) -> &[BloomFilterColumnConfig] { + &self.bloom_filter_columns + } } diff --git a/crates/core/datasets-raw/src/manifest.rs b/crates/core/datasets-raw/src/manifest.rs index 93de50aea..c741fbf05 100644 --- a/crates/core/datasets-raw/src/manifest.rs +++ b/crates/core/datasets-raw/src/manifest.rs @@ -2,7 +2,11 @@ use std::collections::BTreeMap; -use datasets_common::{block_num::BlockNum, manifest::TableSchema, network_id::NetworkId}; +use datasets_common::{ + block_num::BlockNum, + manifest::{BloomFilterColumnConfig, TableSchema}, + network_id::NetworkId, +}; use crate::dataset_kind::{ EvmRpcDatasetKind, FirehoseDatasetKind, SolanaDatasetKind, TempoDatasetKind, @@ -57,6 +61,10 @@ pub struct Table { pub schema: TableSchema, /// Network for this table. pub network: NetworkId, + /// Columns to apply bloom filters to for row-group pruning. + /// Omit to disable bloom filters for this table. + #[serde(default)] + pub bloom_filter_columns: Vec, } /// Schema generation types, gated behind the `schemars` feature. diff --git a/crates/core/worker-core/src/compaction/config.rs b/crates/core/worker-core/src/compaction/config.rs index 72bcd2929..87a8996ff 100644 --- a/crates/core/worker-core/src/compaction/config.rs +++ b/crates/core/worker-core/src/compaction/config.rs @@ -8,8 +8,6 @@ use super::algorithm::Overflow; pub struct ParquetConfig { /// Compression algorithm: zstd, lz4, gzip, brotli, snappy, uncompressed (default: zstd(1)) pub compression: Compression, - /// Enable bloom filters (default: false) - pub bloom_filters: bool, /// Parquet metadata cache size in MB (default: 1024) pub cache_size_mb: u64, /// Max row group size in MB (default: 512) @@ -26,7 +24,6 @@ impl Default for ParquetConfig { fn default() -> Self { Self { compression: Compression::ZSTD(ZstdLevel::default()), - bloom_filters: false, cache_size_mb: 1024, // 1 GB max_row_group_mb: 512, // 512 MB target_size: SizeLimitConfig::default(), diff --git a/crates/core/worker-core/src/lib.rs b/crates/core/worker-core/src/lib.rs index dbe9b6b45..7b790b866 100644 --- a/crates/core/worker-core/src/lib.rs +++ b/crates/core/worker-core/src/lib.rs @@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration}; use datafusion::parquet::file::properties::WriterProperties as ParquetWriterProperties; +use datasets_common::manifest::BloomFilterColumnConfig; pub mod block_ranges; pub mod check; @@ -51,11 +52,6 @@ pub struct WriterProperties { pub fn parquet_opts(config: impl Into) -> Arc { let config = config.into(); - // We have not done our own benchmarking, but the default 1_000_000 value for this adds about a - // megabyte of storage per column, per row group. This analysis by InfluxData suggests that - // smaller NDV values may be equally effective: - // https://www.influxdata.com/blog/using-parquets-bloom-filters/ - let bloom_filter_ndv = 10_000; // For DataFusion defaults, see `ParquetOptions` here: // https://github.com/apache/arrow-datafusion/blob/main/datafusion/common/src/config.rs @@ -63,11 +59,7 @@ pub fn parquet_opts(config: impl Into) -> Arc { // Note: We could set `sorting_columns` for columns like `block_num` and `ordinal`. However, // Datafusion doesn't actually read that metadata info anywhere and just reiles on the // `file_sort_order` set on the reader configuration. - let parquet = ParquetWriterProperties::builder() - .set_compression(config.compression) - .set_bloom_filter_ndv(bloom_filter_ndv) - .set_bloom_filter_enabled(config.bloom_filters) - .build(); + let parquet = build_parquet_writer_properties(config.compression, &[]); let collector = CollectorProperties::from(&config); let compactor = CompactorProperties::from(&config); @@ -88,3 +80,28 @@ pub fn parquet_opts(config: impl Into) -> Arc { } .into() } + +/// Builds `ParquetWriterProperties` with per-column bloom filter configuration. +/// +/// When `bloom_filter_columns` is non-empty, bloom filters are enabled only on the +/// specified columns with their configured NDV values. When empty, no bloom filters +/// are written. +pub fn build_parquet_writer_properties( + compression: datafusion::parquet::basic::Compression, + bloom_filter_columns: &[BloomFilterColumnConfig], +) -> ParquetWriterProperties { + let mut builder = ParquetWriterProperties::builder().set_compression(compression); + + if bloom_filter_columns.is_empty() { + builder.build() + } else { + // Disable bloom filters globally, then enable per-column + builder = builder.set_bloom_filter_enabled(false); + for col in bloom_filter_columns { + builder = builder + .set_column_bloom_filter_enabled(col.column.clone().into(), true) + .set_column_bloom_filter_ndv(col.column.clone().into(), col.ndv); + } + builder.build() + } +} diff --git a/crates/core/worker-datasets-raw/src/job_impl.rs b/crates/core/worker-datasets-raw/src/job_impl.rs index 1edc0a3a8..a06ee6dfc 100644 --- a/crates/core/worker-datasets-raw/src/job_impl.rs +++ b/crates/core/worker-datasets-raw/src/job_impl.rs @@ -83,7 +83,7 @@ use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc, time::Instant}; use amp_data_store::retryable::RetryableErrorExt as _; use amp_providers_registry::retryable::RetryableErrorExt as _; use amp_worker_core::{ - ResolvedEndBlock, + ResolvedEndBlock, WriterProperties, block_ranges::resolve_end_block, check::consistency_check, compaction::AmpCompactor, @@ -142,11 +142,28 @@ pub async fn execute( let dataset_reference = dataset.reference(); let materialize_start_time = Instant::now(); - let parquet_opts = amp_worker_core::parquet_opts(ctx.config.parquet_writer.clone()); + let global_parquet_opts = amp_worker_core::parquet_opts(ctx.config.parquet_writer.clone()); // Initialize physical tables and compactors let mut tables: Vec<(Arc, Arc)> = vec![]; + let mut per_table_opts: BTreeMap> = BTreeMap::new(); for table_def in dataset.tables() { + // Build per-table WriterProperties with table-specific bloom filter columns + let bloom_filter_columns = table_def.bloom_filter_columns(); + let table_opts = if bloom_filter_columns.is_empty() { + global_parquet_opts.clone() + } else { + let table_parquet = amp_worker_core::build_parquet_writer_properties( + ctx.config.parquet_writer.compression, + bloom_filter_columns, + ); + Arc::new(WriterProperties { + parquet: table_parquet, + ..(*global_parquet_opts).clone() + }) + }; + per_table_opts.insert(table_def.name().clone(), table_opts.clone()); + // Try to get existing active physical table (handles retry case) let revision = match ctx .data_store @@ -176,7 +193,7 @@ pub async fn execute( let compactor = AmpCompactor::start( ctx.metadata_db.clone(), ctx.data_store.clone(), - parquet_opts.clone(), + table_opts, physical_table.clone(), ctx.metrics.clone(), ) @@ -367,7 +384,7 @@ pub async fn execute( &client, &ctx, &catalog, - parquet_opts.clone(), + per_table_opts.clone(), missing_ranges_by_table, compactors_by_table, &tables, diff --git a/crates/core/worker-datasets-raw/src/job_impl/ranges.rs b/crates/core/worker-datasets-raw/src/job_impl/ranges.rs index d643135ec..14cde47fa 100644 --- a/crates/core/worker-datasets-raw/src/job_impl/ranges.rs +++ b/crates/core/worker-datasets-raw/src/job_impl/ranges.rs @@ -51,7 +51,7 @@ pub(super) async fn materialize_ranges( client: &S, ctx: &Context, catalog: &Catalog, - parquet_opts: Arc, + per_table_opts: BTreeMap>, missing_ranges_by_table: BTreeMap>>, compactors_by_table: BTreeMap>, tables: &[(Arc, Arc)], @@ -113,7 +113,7 @@ pub(super) async fn materialize_ranges( data_store: ctx.data_store.clone(), catalog: catalog.clone(), ranges, - parquet_opts: parquet_opts.clone(), + per_table_opts: per_table_opts.clone(), missing_ranges_by_table: missing_ranges_by_table.clone(), compactors_by_table: compactors_by_table.clone(), id: i as u32, @@ -418,8 +418,8 @@ struct MaterializePartition { catalog: Catalog, /// The block ranges to scan ranges: Vec>, - /// The Parquet writer properties - parquet_opts: Arc, + /// Per-table Parquet writer properties + per_table_opts: BTreeMap>, /// The missing block ranges by table missing_ranges_by_table: BTreeMap>>, /// The compactors for each table @@ -505,7 +505,7 @@ impl MaterializePartition { self.catalog.clone(), self.metadata_db.clone(), self.data_store.clone(), - self.parquet_opts.clone(), + self.per_table_opts.clone(), missing_ranges_by_table, self.compactors_by_table.clone(), self.metrics.clone(), diff --git a/crates/core/worker-datasets-raw/src/job_impl/writer.rs b/crates/core/worker-datasets-raw/src/job_impl/writer.rs index 286b31206..56ad5cffd 100644 --- a/crates/core/worker-datasets-raw/src/job_impl/writer.rs +++ b/crates/core/worker-datasets-raw/src/job_impl/writer.rs @@ -34,22 +34,23 @@ impl RawDatasetWriter { catalog: Catalog, metadata_db: MetadataDb, store: DataStore, - opts: Arc, + per_table_opts: BTreeMap>, missing_ranges_by_table: BTreeMap>>, compactors_by_table: BTreeMap>, metrics: Option>, ) -> Result { let mut writers = BTreeMap::new(); for table in catalog.physical_tables() { - // Unwrap: `missing_ranges_by_table` contains an entry for each table. + // Unwrap: `missing_ranges_by_table` and `per_table_opts` contain an entry for each table. let table_name = table.table_name(); let ranges = missing_ranges_by_table.get(table_name).unwrap().clone(); let compactor = Arc::clone(compactors_by_table.get(table_name).unwrap()); + let opts = Arc::clone(per_table_opts.get(table_name).unwrap()); let writer = RawTableWriter::new( table.clone(), store.clone(), compactor, - opts.clone(), + opts, ranges, metrics.clone(), )?; diff --git a/crates/extractors/evm-rpc/src/lib.rs b/crates/extractors/evm-rpc/src/lib.rs index 8a54f8bf4..1ab795c3e 100644 --- a/crates/extractors/evm-rpc/src/lib.rs +++ b/crates/extractors/evm-rpc/src/lib.rs @@ -21,6 +21,7 @@ pub fn dataset(reference: HashReference, manifest: Manifest) -> RawDataset { manifest.kind.into(), network.clone(), tables::all(&network), + &manifest.tables, Some(manifest.start_block), manifest.finalized_blocks_only, ) diff --git a/crates/extractors/firehose/src/lib.rs b/crates/extractors/firehose/src/lib.rs index b0fa2bff8..e11fb825f 100644 --- a/crates/extractors/firehose/src/lib.rs +++ b/crates/extractors/firehose/src/lib.rs @@ -25,6 +25,7 @@ pub fn dataset(reference: HashReference, manifest: Manifest) -> RawDataset { manifest.kind.into(), network.clone(), tables::all(&network), + &manifest.tables, Some(manifest.start_block), manifest.finalized_blocks_only, ) diff --git a/crates/extractors/solana/src/lib.rs b/crates/extractors/solana/src/lib.rs index 01bdf4fc9..4f7ae542e 100644 --- a/crates/extractors/solana/src/lib.rs +++ b/crates/extractors/solana/src/lib.rs @@ -21,6 +21,7 @@ pub fn dataset(reference: HashReference, manifest: Manifest) -> RawDataset { manifest.kind.into(), network.clone(), tables::all(&network), + &manifest.tables, Some(manifest.start_block), manifest.finalized_blocks_only, ) diff --git a/crates/extractors/tempo/src/lib.rs b/crates/extractors/tempo/src/lib.rs index 337d37684..970aad2af 100644 --- a/crates/extractors/tempo/src/lib.rs +++ b/crates/extractors/tempo/src/lib.rs @@ -18,6 +18,7 @@ pub fn dataset(reference: HashReference, manifest: Manifest) -> RawDataset { manifest.kind.into(), network.clone(), tables::all(&network), + &manifest.tables, Some(manifest.start_block), manifest.finalized_blocks_only, ) diff --git a/docs/config.sample.toml b/docs/config.sample.toml index b403214bb..6990b3013 100644 --- a/docs/config.sample.toml +++ b/docs/config.sample.toml @@ -70,7 +70,6 @@ url = "postgres://" # Writer/Parquet configuration [writer] # compression = "zstd(1)" # Compression algorithm: zstd, lz4, gzip, brotli, snappy, uncompressed (default: zstd(1)) -# bloom_filters = false # Enable bloom filters (default: false) # cache_size_mb = 1024 # Parquet metadata cache size in MB (default: 1024) # max_row_group_mb = 512 # Max row group size in MB (default: 512) # segment_flush_interval_secs = 600.0 # Max wall-clock seconds before closing a segment (default: 600 = 10 min) diff --git a/docs/schemas/config/ampd.spec.json b/docs/schemas/config/ampd.spec.json index 528a468c5..a29d04062 100644 --- a/docs/schemas/config/ampd.spec.json +++ b/docs/schemas/config/ampd.spec.json @@ -318,11 +318,6 @@ "description": "Parquet writer and file configuration.\n\nControls compression, caching, segment sizing, compaction, and garbage collection\nfor Parquet files produced by the worker.", "type": "object", "properties": { - "bloom_filters": { - "description": "Enable Parquet bloom filters (default: false).", - "type": "boolean", - "default": false - }, "bytes": { "description": "Target bytes per file (default: 2147483648 = 2 GB for target_size, 0 for eager limits).", "type": "integer", diff --git a/docs/schemas/manifest/raw.spec.json b/docs/schemas/manifest/raw.spec.json index 59b6b0017..8ed07954f 100644 --- a/docs/schemas/manifest/raw.spec.json +++ b/docs/schemas/manifest/raw.spec.json @@ -60,6 +60,26 @@ "fields" ] }, + "BloomFilterColumnConfig": { + "description": "Configuration for a bloom filter on a specific column.\n\nBloom filters enable efficient row-group pruning during queries by quickly\ndetermining whether a row group could contain a specific value. The `ndv`\n(number of distinct values) parameter tunes the filter's false-positive rate.", + "type": "object", + "properties": { + "column": { + "description": "Column name to apply the bloom filter to.", + "type": "string" + }, + "ndv": { + "description": "Estimated number of distinct values for this column (default: 10,000).", + "type": "integer", + "format": "uint64", + "default": 10000, + "minimum": 0 + } + }, + "required": [ + "column" + ] + }, "DataType": { "description": "Arrow data type, e.g. `Int32`, `Utf8`, etc.", "type": "string" @@ -106,6 +126,14 @@ "description": "Table definition for raw dataset manifests.", "type": "object", "properties": { + "bloom_filter_columns": { + "description": "Columns to apply bloom filters to for row-group pruning.\nOmit to disable bloom filters for this table.", + "type": "array", + "default": [], + "items": { + "$ref": "#/$defs/BloomFilterColumnConfig" + } + }, "network": { "description": "Network for this table.", "$ref": "#/$defs/NetworkId" diff --git a/tests/config/manifests/eth_rpc.json b/tests/config/manifests/eth_rpc.json index fa0782b22..0048ab80d 100644 --- a/tests/config/manifests/eth_rpc.json +++ b/tests/config/manifests/eth_rpc.json @@ -173,7 +173,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] }, "logs": { "schema": { @@ -266,7 +267,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] }, "transactions": { "schema": { @@ -567,7 +569,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] } } } diff --git a/tests/config/manifests/solana.json b/tests/config/manifests/solana.json index 73830cfe7..4b2b0c817 100644 --- a/tests/config/manifests/solana.json +++ b/tests/config/manifests/solana.json @@ -46,7 +46,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] }, "block_rewards": { "schema": { @@ -90,7 +91,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] }, "instructions": { "schema": { @@ -157,7 +159,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] }, "messages": { "schema": { @@ -291,7 +294,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] }, "transactions": { "schema": { @@ -668,7 +672,8 @@ ] } }, - "network": "mainnet" + "network": "mainnet", + "bloom_filter_columns": [] } } -} \ No newline at end of file +} diff --git a/tests/config/manifests/tempo.json b/tests/config/manifests/tempo.json index 13c764d95..901b9bf30 100644 --- a/tests/config/manifests/tempo.json +++ b/tests/config/manifests/tempo.json @@ -188,7 +188,8 @@ ] } }, - "network": "testnet" + "network": "testnet", + "bloom_filter_columns": [] }, "logs": { "schema": { @@ -281,7 +282,8 @@ ] } }, - "network": "testnet" + "network": "testnet", + "bloom_filter_columns": [] }, "transactions": { "schema": { @@ -992,7 +994,8 @@ ] } }, - "network": "testnet" + "network": "testnet", + "bloom_filter_columns": [] } } -} \ No newline at end of file +}