Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ src/
├── args/ # clap Args struct + CLI value types (consistency, ordering)
├── config/ # YAML configs: upload (collection/vector/payload), search, schema reference
├── client.rs # Qdrant client construction + multi-client retry
├── collection/ # collection (re)creation: from CLI flags / from YAML config
├── collection/ # collection (re)creation, index wait, timed field-index creation
├── generators/ # data generation: points (legacy & config), queries, random primitives
├── search/ # search benchmarking processors (flag-driven & config-driven)
├── upload.rs # upload pipeline (parallelism, batching, progress)
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,40 @@ Read over Qdrant's **REST** API, on the port derived from `--uri` (6334 → 6333
A failure here warns rather than failing a run that already has its headline
number; `--skip-server-stats` turns it off.

### `create-field-index` — time payload field-index construction

Field indices are normally created together with the collection, while it is
still empty, so building them costs nothing and measures nothing. To measure
index construction over real data, defer them and build them afterwards:

```bash
bfb upload --file config.yaml -n 1M --skip-field-indices
bfb create-field-index --file config.yaml --json field-index.json
```

```
--- Field index creation ---
color (keyword): 0.316 s (server 0.277 s)
price (integer): 0.316 s (server 0.287 s)
Total: 0.632 s
```

Each index is created with `wait=true`, so the elapsed time reflects the build
rather than the request round-trip. Fields declared `index: false` are never
built — that is how unindexed filler payload is expressed. Per-field timings
land under `results.create_field_index` in `--json`.

Indices are built one after another, so `Total` is the number to compare across
runs; a per-field time also carries whatever the fields before it left warm. To
attribute a cost to one field, build it alone, and use `drop-field-index` to re-measure
it on the same data:

```bash
bfb create-field-index --file config.yaml --field color
bfb drop-field-index --file config.yaml --field color
bfb drop-field-index --file config.yaml # or every indexed field in the config
```

### `schema` — print the upload-config file schema

Print an annotated YAML reference enumerating every option accepted by an
Expand Down Expand Up @@ -237,6 +271,8 @@ bfb -n 1M --search --json results.json
}
```

`config.num_vectors` is the point (or query) count the run was *asked* for, from `-n` (or the CLI default) — not what the collection turned out to hold. It is omitted entirely for phase-only commands like `bfb create-field-index`, which neither upload nor query.

Phases that did not run are omitted: `bfb search --file …` yields only
`results.search`, and `precision` appears only when accuracy was measured
(`--search-quality`, or a dataset query source with ground truth). Timings are
Expand Down
37 changes: 37 additions & 0 deletions src/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,11 +449,48 @@ pub enum Command {
/// *how* the benchmark runs.
Scroll(ScrollArgs),

/// Build payload field indices on an existing, populated collection, and
/// time each one.
///
/// Upload with `--skip-field-indices` first, then run this to measure index
/// construction over real data rather than on an empty collection.
CreateFieldIndex(CreateFieldIndexArgs),

/// Drop payload field indices, so they can be rebuilt and re-measured on
/// the same data without re-uploading.
DropFieldIndex(DropFieldIndexArgs),

/// Print the upload-config file schema: every option, its type, default,
/// and allowed values, as an annotated YAML reference.
Schema,
}

#[derive(clap::Args, Debug, Clone)]
pub struct CreateFieldIndexArgs {
/// Path to the YAML collection-shape config file: every field it declares
/// with `index: true` is built.
#[clap(long)]
pub file: String,

/// Build only this field's index. Repeatable. Timings for several fields
/// built together are order-dependent, so isolate a field by building it
/// alone (pair with `bfb drop-field-index` to repeat on the same data).
#[clap(long)]
pub field: Vec<String>,
}

#[derive(clap::Args, Debug, Clone)]
pub struct DropFieldIndexArgs {
/// Path to the YAML collection-shape config file: every field it declares
/// with `index: true` is dropped, unless narrowed with `--field`.
#[clap(long)]
pub file: String,

/// Drop only this field's index. Repeatable.
#[clap(long)]
pub field: Vec<String>,
}

#[derive(clap::Args, Debug, Clone)]
pub struct SearchArgs {
/// Path to the YAML search-request config file
Expand Down
190 changes: 75 additions & 115 deletions src/collection/from_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use qdrant_client::qdrant::{
};
use tokio::time::sleep;

use super::{FieldIndexSpec, create_field_indices};
use crate::args::{Args, QuantizationArg};
use crate::client::random_client;
use crate::generators::random::{
Expand Down Expand Up @@ -258,7 +259,7 @@ pub async fn recreate_collection(args: &Args, stopped: Arc<AtomicBool>) -> Resul
sleep(Duration::from_secs(1)).await;

if !args.skip_field_indices {
create_field_indices(args, &client).await?;
create_field_indices(&client, field_index_specs(args)).await?;
}

if let Some(shard_key) = &args.shard_key {
Expand All @@ -279,153 +280,112 @@ pub async fn recreate_collection(args: &Args, stopped: Arc<AtomicBool>) -> Resul
Ok(())
}

async fn create_field_indices(args: &Args, client: &qdrant_client::Qdrant) -> Result<()> {
/// Build the field-index requests implied by the CLI flags.
///
/// Pure: performs no I/O, so a bad spec is reported rather than panicking
/// mid-creation.
fn field_index_specs(args: &Args) -> Vec<FieldIndexSpec> {
let collection = args.collection_name.clone();
let on_disk = args.on_disk_payload_index;
let tenant = args.tenants.unwrap_or_default();
let mut specs = Vec::new();

for (idx, _) in args.keywords.iter().enumerate() {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
format!("{}{}", payload_prefixes(idx), KEYWORD_PAYLOAD_KEY),
FieldType::Keyword,
)
let field = format!("{}{}", payload_prefixes(idx), KEYWORD_PAYLOAD_KEY);
specs.push(FieldIndexSpec::new(
&field,
"keyword",
CreateFieldIndexCollectionBuilder::new(&collection, &field, FieldType::Keyword)
.field_index_params(
KeywordIndexParamsBuilder::default()
.on_disk(args.on_disk_payload_index)
.is_tenant(args.tenants.unwrap_or_default()),
)
.wait(true),
)
.await
.unwrap();
.on_disk(on_disk)
.is_tenant(tenant),
),
));
}

for (idx, _) in args.float_payloads.iter().enumerate() {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
format!("{}{}", payload_prefixes(idx), FLOAT_PAYLOAD_KEY),
FieldType::Float,
)
let field = format!("{}{}", payload_prefixes(idx), FLOAT_PAYLOAD_KEY);
specs.push(FieldIndexSpec::new(
&field,
"float",
CreateFieldIndexCollectionBuilder::new(&collection, &field, FieldType::Float)
.field_index_params(
FloatIndexParamsBuilder::default()
.on_disk(args.on_disk_payload_index)
.is_principal(args.tenants.unwrap_or_default()),
)
.wait(true),
)
.await
.unwrap();
.on_disk(on_disk)
.is_principal(tenant),
),
));
}

for (idx, _) in args.int_payloads.iter().enumerate() {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
format!("{}{}", payload_prefixes(idx), INTEGERS_PAYLOAD_KEY),
FieldType::Integer,
)
let field = format!("{}{}", payload_prefixes(idx), INTEGERS_PAYLOAD_KEY);
specs.push(FieldIndexSpec::new(
&field,
"integer",
CreateFieldIndexCollectionBuilder::new(&collection, &field, FieldType::Integer)
.field_index_params(
IntegerIndexParamsBuilder::new(true, args.int_payloads_range)
.on_disk(args.on_disk_payload_index)
.is_principal(args.tenants.unwrap_or_default()),
)
.wait(true),
)
.await
.unwrap();
.on_disk(on_disk)
.is_principal(tenant),
),
));
}

if args.timestamp_payload {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
"timestamp",
FieldType::Datetime,
)
specs.push(FieldIndexSpec::new(
"timestamp",
"datetime",
CreateFieldIndexCollectionBuilder::new(&collection, "timestamp", FieldType::Datetime)
.field_index_params(
DatetimeIndexParamsBuilder::default()
.on_disk(args.on_disk_payload_index)
.is_principal(args.tenants.unwrap_or_default()),
)
.wait(true),
)
.await
.unwrap();
.on_disk(on_disk)
.is_principal(tenant),
),
));
}

if args.uuid_payloads {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
UUID_PAYLOAD_KEY,
FieldType::Uuid,
)
specs.push(FieldIndexSpec::new(
UUID_PAYLOAD_KEY,
"uuid",
CreateFieldIndexCollectionBuilder::new(&collection, UUID_PAYLOAD_KEY, FieldType::Uuid)
.field_index_params(
UuidIndexParamsBuilder::default()
.is_tenant(args.tenants.unwrap_or_default())
.on_disk(args.on_disk_payload_index),
)
.wait(true),
)
.await
.unwrap();
.is_tenant(tenant)
.on_disk(on_disk),
),
));
}

if args.geo_payloads {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
GEO_PAYLOAD_KEY,
FieldType::Geo,
)
.field_index_params(
GeoIndexParamsBuilder::new().on_disk(args.on_disk_payload_index),
)
.wait(true),
)
.await
.unwrap();
specs.push(FieldIndexSpec::new(
GEO_PAYLOAD_KEY,
"geo",
CreateFieldIndexCollectionBuilder::new(&collection, GEO_PAYLOAD_KEY, FieldType::Geo)
.field_index_params(GeoIndexParamsBuilder::new().on_disk(on_disk)),
));
}

if args.bool_payloads {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
BOOL_PAYLOAD_KEY,
FieldType::Bool,
)
.field_index_params(
BoolIndexParamsBuilder::default().on_disk(args.on_disk_payload_index),
)
.wait(true),
)
.await
.unwrap();
specs.push(FieldIndexSpec::new(
BOOL_PAYLOAD_KEY,
"bool",
CreateFieldIndexCollectionBuilder::new(&collection, BOOL_PAYLOAD_KEY, FieldType::Bool)
.field_index_params(BoolIndexParamsBuilder::default().on_disk(on_disk)),
));
}

if args.text_payloads {
client
.create_field_index(
CreateFieldIndexCollectionBuilder::new(
args.collection_name.clone(),
TEXT_PAYLOAD_KEY,
FieldType::Text,
)
specs.push(FieldIndexSpec::new(
TEXT_PAYLOAD_KEY,
"text",
CreateFieldIndexCollectionBuilder::new(&collection, TEXT_PAYLOAD_KEY, FieldType::Text)
.field_index_params(
TextIndexParamsBuilder::new(TokenizerType::Word)
.on_disk(args.on_disk_payload_index),
)
.wait(true),
)
.await
.unwrap();
TextIndexParamsBuilder::new(TokenizerType::Word).on_disk(on_disk),
),
));
}

Ok(())
specs
}
Loading