use anyhow::Result;
use qdrant_client::Qdrant; use std::sync::Arc;
use clap::{Parser, Subcommand};
use qdrant_client::{
qdrant::{
FieldType, PointStruct, TextIndexParams, KeywordIndexParams, IntegerIndexParams,
FloatIndexParams, GeoIndexParams, BoolIndexParams, DatetimeIndexParams,
UuidIndexParams, TokenizerType, UpdateStatus,
UpsertPointsBuilder,
UpsertPoints,
CreateFieldIndexCollectionBuilder,
},
};
use indicatif;
use crate::config::AppConfig;
pub(crate) const BATCH_SIZE: usize = 128;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct CliArgs {
#[command(subcommand)]
pub command: Commands,
#[arg(short = 'm', long = "onnx-model", global = true, env = "VECTORDB_ONNX_MODEL")]
pub onnx_model_path_arg: Option<String>,
#[arg(short = 't', long = "onnx-tokenizer-dir", global = true, env = "VECTORDB_ONNX_TOKENIZER_DIR")]
pub onnx_tokenizer_dir_arg: Option<String>,
}
pub const FIELD_FILE_PATH: &str = "file_path";
pub const FIELD_START_LINE: &str = "start_line";
pub const FIELD_END_LINE: &str = "end_line";
pub const FIELD_FILE_EXTENSION: &str = "file_extension";
pub const FIELD_LANGUAGE: &str = "language";
pub const FIELD_ELEMENT_TYPE: &str = "element_type";
pub const FIELD_CHUNK_CONTENT: &str = "chunk_content";
pub const FIELD_BRANCH: &str = "branch";
pub const FIELD_COMMIT_HASH: &str = "commit_hash";
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(subcommand_negates_reqs = true)]
Index(super::index::IndexArgs), #[command(subcommand_negates_reqs = true)]
Query(super::query::QueryArgs), #[command(subcommand_negates_reqs = true)]
Stats(super::stats::StatsArgs), #[command(subcommand_negates_reqs = true)]
List(super::list::ListArgs), #[command(subcommand_negates_reqs = true)]
Clear(super::clear::ClearArgs), #[command(subcommand_negates_reqs = true)]
Repo(super::repo_commands::RepoArgs),
}
pub async fn handle_command(
args: CliArgs,
config: AppConfig,
client: Arc<Qdrant>,
) -> Result<()> {
match args.command {
Commands::Index(ref cmd_args) => super::index::handle_index(cmd_args, &args, config, client).await,
Commands::Query(ref cmd_args) => super::query::handle_query(cmd_args, &args, config, client).await,
Commands::Stats(cmd_args) => super::stats::handle_stats(cmd_args, config, client).await,
Commands::List(cmd_args) => super::list::handle_list(cmd_args, config, client).await,
Commands::Clear(cmd_args) => super::clear::handle_clear(cmd_args, config, client).await,
Commands::Repo(ref cmd_args) => super::repo_commands::handle_repo_command(cmd_args.clone(), &args, config, client).await,
}
}
pub async fn ensure_payload_index(
client: &Qdrant,
collection_name: &str,
field_name: &str,
field_type: FieldType,
) -> Result<()> {
let request_builder = CreateFieldIndexCollectionBuilder::new(collection_name, field_name, field_type);
let final_request = match field_type {
FieldType::Keyword => request_builder.field_index_params(KeywordIndexParams::default()).build(),
FieldType::Integer => request_builder.field_index_params(IntegerIndexParams::default()).build(),
FieldType::Float => request_builder.field_index_params(FloatIndexParams::default()).build(),
FieldType::Geo => request_builder.field_index_params(GeoIndexParams::default()).build(),
FieldType::Text => request_builder.field_index_params(TextIndexParams {
tokenizer: TokenizerType::Word.into(),
lowercase: Some(true),
min_token_len: None,
max_token_len: None,
on_disk: None,
}).build(),
FieldType::Bool => request_builder.field_index_params(BoolIndexParams::default()).build(),
FieldType::Datetime => request_builder.field_index_params(DatetimeIndexParams::default()).build(),
FieldType::Uuid => request_builder.field_index_params(UuidIndexParams::default()).build(),
};
match client.create_field_index(final_request).await {
Ok(_) => {
log::info!("Successfully created or confirmed index for field '{}'", field_name);
Ok(())
},
Err(e) => {
let error_string = e.to_string();
if error_string.contains("already exists") || error_string.contains("exists already") {
log::warn!("Index for field '{}' already exists.", field_name);
Ok(())
} else if error_string.contains("Collection") && error_string.contains("not found") {
log::error!("Cannot create index because collection '{}' does not exist.", collection_name);
Err(anyhow::anyhow!("Collection '{}' not found when creating index for '{}'.", collection_name, field_name).context(e))
} else {
Err(anyhow::anyhow!("Failed to create index for field '{}'", field_name).context(e))
}
}
}
}
pub(crate) async fn upsert_batch(
client: &Qdrant,
collection_name: &str,
points: Vec<PointStruct>,
pb: &indicatif::ProgressBar,
) -> Result<()> {
if points.is_empty() {
return Ok(());
}
let count = points.len();
log::debug!("Upserting batch of {} points to {}", count, collection_name);
pb.set_message(format!("Upserting {} points...", count));
let request: UpsertPoints = UpsertPointsBuilder::new(collection_name, points)
.wait(true) .build();
match client.upsert_points(request).await {
Ok(response) => {
if let Some(result) = response.result {
match UpdateStatus::try_from(result.status) {
Ok(UpdateStatus::Completed) => {
log::debug!("Upsert batch successful.");
Ok(())
},
Ok(status) => {
let msg = format!("Qdrant upsert batch completed with status: {:?}", status);
pb.println(format!("Warning: {}", msg));
log::warn!("{}", msg);
Ok(()) },
Err(_) => {
let msg = format!("Qdrant upsert batch completed with unknown status code: {}", result.status);
pb.println(format!("Error: {}", msg));
log::error!("{}", msg);
Err(anyhow::anyhow!(msg))
}
}
} else {
let msg = "Qdrant upsert response missing result status";
pb.println(format!("Error: {}", msg));
log::error!("{}", msg);
Err(anyhow::anyhow!(msg))
}
}
Err(e) => {
let msg = format!("Failed to upsert batch to {}: {}", collection_name, e);
pb.println(format!("Error: {}", msg));
log::error!("{}", msg);
Err(anyhow::anyhow!(msg).context(e))
}
}
}