mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-06 01:50:57 -05:00
* build(deps): bump rand_distr from 0.5.1 to 0.6.0 Bumps [rand_distr](https://github.com/rust-random/rand_distr) from 0.5.1 to 0.6.0. - [Release notes](https://github.com/rust-random/rand_distr/releases) - [Changelog](https://github.com/rust-random/rand_distr/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-random/rand_distr/compare/0.5.1...0.6.0) --- updated-dependencies: - dependency-name: rand_distr dependency-version: 0.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Migrate main code base to rand 0.10 * Migrate tests * Migrate benches --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: timvisee <tim@visee.me>
674 lines
20 KiB
Rust
674 lines
20 KiB
Rust
use collection::collection::Collection;
|
|
use collection::grouping::group_by::{GroupRequest, SourceRequest};
|
|
use collection::operations::CollectionUpdateOperations;
|
|
use collection::operations::point_ops::WriteOrdering;
|
|
use collection::operations::types::{RecommendRequestInternal, UpdateStatus};
|
|
use itertools::Itertools;
|
|
use rand::RngExt;
|
|
use rand::distr::Uniform;
|
|
use rand::rngs::ThreadRng;
|
|
use segment::data_types::vectors::DenseVector;
|
|
use segment::json_path::JsonPath;
|
|
use segment::types::{Filter, WithPayloadInterface, WithVector};
|
|
use serde_json::json;
|
|
|
|
use crate::common::simple_collection_fixture;
|
|
|
|
fn rand_dense_vector(rng: &mut ThreadRng, size: usize) -> DenseVector {
|
|
rng.sample_iter(Uniform::new(0.4, 0.6).unwrap())
|
|
.take(size)
|
|
.collect()
|
|
}
|
|
|
|
mod group_by {
|
|
use api::rest::SearchRequestInternal;
|
|
use collection::grouping::GroupBy;
|
|
use collection::operations::point_ops::{
|
|
BatchPersisted, BatchVectorStructPersisted, PointInsertOperationsInternal, PointOperations,
|
|
};
|
|
use common::counter::hardware_accumulator::HwMeasurementAcc;
|
|
use segment::payload_json;
|
|
|
|
use super::*;
|
|
|
|
struct Resources {
|
|
request: GroupRequest,
|
|
collection: Collection,
|
|
}
|
|
|
|
async fn setup(docs: u64, chunks: u64) -> Resources {
|
|
let mut rng = rand::rng();
|
|
|
|
let source = SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
});
|
|
|
|
let request = GroupRequest::with_limit_from_request(source, JsonPath::new("docId"), 3);
|
|
|
|
let collection_dir = tempfile::Builder::new()
|
|
.prefix("collection")
|
|
.tempdir()
|
|
.unwrap();
|
|
|
|
let collection = simple_collection_fixture(collection_dir.path(), 1).await;
|
|
|
|
let batch = BatchPersisted {
|
|
ids: (0..docs * chunks).map(|x| x.into()).collect_vec(),
|
|
vectors: BatchVectorStructPersisted::Single(
|
|
(0..docs * chunks)
|
|
.map(|_| rand_dense_vector(&mut rng, 4))
|
|
.collect_vec(),
|
|
),
|
|
payloads: (0..docs)
|
|
.flat_map(|x| {
|
|
(0..chunks).map(move |_| {
|
|
Some(payload_json! { "docId": x , "other_stuff": x.to_string() + "foo" })
|
|
})
|
|
})
|
|
.collect_vec()
|
|
.into(),
|
|
};
|
|
|
|
let insert_points = CollectionUpdateOperations::PointOperation(
|
|
PointOperations::UpsertPoints(PointInsertOperationsInternal::from(batch)),
|
|
);
|
|
|
|
let hw_counter = HwMeasurementAcc::new();
|
|
let insert_result = collection
|
|
.update_from_client_simple(
|
|
insert_points,
|
|
true,
|
|
None,
|
|
WriteOrdering::default(),
|
|
hw_counter.clone(),
|
|
)
|
|
.await
|
|
.expect("insert failed");
|
|
|
|
assert_eq!(insert_result.status, UpdateStatus::Completed);
|
|
|
|
Resources {
|
|
request,
|
|
collection,
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn searching() {
|
|
let resources = setup(16, 8).await;
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
resources.request.clone(),
|
|
&resources.collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
let group_req = resources.request;
|
|
|
|
assert_eq!(result.len(), group_req.limit);
|
|
assert_eq!(result[0].hits.len(), group_req.group_size);
|
|
|
|
// is sorted?
|
|
let mut last_group_best_score = f32::MAX;
|
|
for group in result {
|
|
assert!(group.hits[0].score <= last_group_best_score);
|
|
last_group_best_score = group.hits[0].score;
|
|
|
|
let mut last_score = f32::MAX;
|
|
for hit in group.hits {
|
|
assert!(hit.score <= last_score);
|
|
last_score = hit.score;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn recommending() {
|
|
let resources = setup(16, 8).await;
|
|
|
|
let request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Recommend(RecommendRequestInternal {
|
|
strategy: Default::default(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
positive: vec![1.into(), 2.into(), 3.into()],
|
|
negative: Vec::new(),
|
|
using: None,
|
|
lookup_from: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
2,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
request.clone(),
|
|
&resources.collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), request.limit);
|
|
|
|
let mut last_group_best_score = f32::MAX;
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), request.group_size);
|
|
|
|
// is sorted?
|
|
assert!(group.hits[0].score <= last_group_best_score);
|
|
last_group_best_score = group.hits[0].score;
|
|
|
|
let mut last_score = f32::MAX;
|
|
for hit in group.hits {
|
|
assert!(hit.score <= last_score);
|
|
last_score = hit.score;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn with_filter() {
|
|
let resources = setup(16, 8).await;
|
|
|
|
let filter: Filter = serde_json::from_value(json!({
|
|
"must": [
|
|
{
|
|
"key": "docId",
|
|
"range": {
|
|
"gte": 1,
|
|
"lte": 2
|
|
}
|
|
}
|
|
]
|
|
}))
|
|
.unwrap();
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: Some(filter.clone()),
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
3,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request,
|
|
&resources.collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), 2);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn with_payload_and_vectors() {
|
|
let resources = setup(16, 8).await;
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: Some(WithPayloadInterface::Bool(true)),
|
|
with_vector: Some(WithVector::Bool(true)),
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
3,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request.clone(),
|
|
&resources.collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), 4);
|
|
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), group_by_request.group_size);
|
|
assert!(group.hits[0].payload.is_some());
|
|
assert!(group.hits[0].vector.is_some());
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn group_by_string_field() {
|
|
let Resources { collection, .. } = setup(16, 8).await;
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: Some(WithPayloadInterface::Bool(true)),
|
|
with_vector: Some(WithVector::Bool(true)),
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("other_stuff"),
|
|
3,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request.clone(),
|
|
&collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), 4);
|
|
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), group_by_request.group_size);
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn zero_group_size() {
|
|
let Resources { collection, .. } = setup(16, 8).await;
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
0,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request.clone(),
|
|
&collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), 0);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn zero_limit_groups() {
|
|
let Resources { collection, .. } = setup(16, 8).await;
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 0,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
3,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request.clone(),
|
|
&collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), 0);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn big_limit_groups() {
|
|
let Resources { collection, .. } = setup(1000, 5).await;
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 500,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
3,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request.clone(),
|
|
&collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), group_by_request.limit);
|
|
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), group_by_request.group_size);
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn big_group_size_groups() {
|
|
let Resources { collection, .. } = setup(10, 500).await;
|
|
|
|
let group_by_request = GroupRequest::with_limit_from_request(
|
|
SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 3,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
}),
|
|
JsonPath::new("docId"),
|
|
400,
|
|
);
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let group_by = GroupBy::new(
|
|
group_by_request.clone(),
|
|
&collection,
|
|
|_| async { unreachable!() },
|
|
hw_acc,
|
|
);
|
|
|
|
let result = group_by.execute().await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), group_by_request.limit);
|
|
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), group_by_request.group_size);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tests out the different features working together. The individual features are already tested in other places.
|
|
mod group_by_builder {
|
|
use std::sync::Arc;
|
|
|
|
use api::rest::SearchRequestInternal;
|
|
use collection::grouping::GroupBy;
|
|
use collection::lookup::WithLookup;
|
|
use collection::lookup::types::PseudoId;
|
|
use collection::operations::point_ops::{
|
|
BatchPersisted, BatchVectorStructPersisted, PointInsertOperationsInternal, PointOperations,
|
|
};
|
|
use common::counter::hardware_accumulator::HwMeasurementAcc;
|
|
use segment::json_path::JsonPath;
|
|
use segment::payload_json;
|
|
|
|
use super::*;
|
|
|
|
const BODY_TEXT: &str = "lorem ipsum dolor sit amet";
|
|
|
|
struct Resources {
|
|
request: GroupRequest,
|
|
lookup_collection: Arc<Collection>,
|
|
collection: Collection,
|
|
}
|
|
|
|
/// Sets up two collections: one for chunks and one for docs.
|
|
async fn setup(docs: u64, chunks_per_doc: u64) -> Resources {
|
|
let mut rng = rand::rng();
|
|
|
|
let source_request = SourceRequest::Search(SearchRequestInternal {
|
|
vector: vec![0.5, 0.5, 0.5, 0.5].into(),
|
|
filter: None,
|
|
params: None,
|
|
limit: 4,
|
|
offset: None,
|
|
with_payload: None,
|
|
with_vector: None,
|
|
score_threshold: None,
|
|
});
|
|
|
|
let request =
|
|
GroupRequest::with_limit_from_request(source_request, JsonPath::new("docId"), 3);
|
|
|
|
let collection_dir = tempfile::Builder::new().prefix("chunks").tempdir().unwrap();
|
|
let collection = simple_collection_fixture(collection_dir.path(), 1).await;
|
|
|
|
let hw_counter = HwMeasurementAcc::new();
|
|
|
|
// insert chunk points
|
|
{
|
|
let batch = BatchPersisted {
|
|
ids: (0..docs * chunks_per_doc).map(|x| x.into()).collect_vec(),
|
|
vectors: BatchVectorStructPersisted::Single(
|
|
(0..docs * chunks_per_doc)
|
|
.map(|_| rand_dense_vector(&mut rng, 4))
|
|
.collect_vec(),
|
|
),
|
|
payloads: (0..docs)
|
|
.flat_map(|x| {
|
|
(0..chunks_per_doc).map(move |_| Some(payload_json! {"docId": x}))
|
|
})
|
|
.collect_vec()
|
|
.into(),
|
|
};
|
|
|
|
let insert_points = CollectionUpdateOperations::PointOperation(
|
|
PointOperations::UpsertPoints(PointInsertOperationsInternal::from(batch)),
|
|
);
|
|
|
|
let insert_result = collection
|
|
.update_from_client_simple(
|
|
insert_points,
|
|
true,
|
|
None,
|
|
WriteOrdering::default(),
|
|
hw_counter.clone(),
|
|
)
|
|
.await
|
|
.expect("insert failed");
|
|
|
|
assert_eq!(insert_result.status, UpdateStatus::Completed);
|
|
}
|
|
|
|
let lookup_dir = tempfile::Builder::new().prefix("lookup").tempdir().unwrap();
|
|
let lookup_collection = simple_collection_fixture(lookup_dir.path(), 1).await;
|
|
|
|
// insert doc points
|
|
{
|
|
let batch = BatchPersisted {
|
|
ids: (0..docs).map(|x| x.into()).collect_vec(),
|
|
vectors: BatchVectorStructPersisted::Single(
|
|
(0..docs)
|
|
.map(|_| rand_dense_vector(&mut rng, 4))
|
|
.collect_vec(),
|
|
),
|
|
payloads: (0..docs)
|
|
.map(|x| Some(payload_json! {"docId": x, "body": format!("{x} {BODY_TEXT}")}))
|
|
.collect_vec()
|
|
.into(),
|
|
};
|
|
|
|
let insert_points = CollectionUpdateOperations::PointOperation(
|
|
PointOperations::UpsertPoints(PointInsertOperationsInternal::from(batch)),
|
|
);
|
|
let insert_result = lookup_collection
|
|
.update_from_client_simple(
|
|
insert_points,
|
|
true,
|
|
None,
|
|
WriteOrdering::default(),
|
|
hw_counter.clone(),
|
|
)
|
|
.await
|
|
.expect("insert failed");
|
|
|
|
assert_eq!(insert_result.status, UpdateStatus::Completed);
|
|
}
|
|
|
|
let lookup_collection = Arc::new(lookup_collection);
|
|
|
|
Resources {
|
|
request,
|
|
lookup_collection,
|
|
collection,
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn only_group_by() {
|
|
let Resources {
|
|
request,
|
|
collection,
|
|
..
|
|
} = setup(16, 8).await;
|
|
|
|
let collection_by_name = |_: String| async { unreachable!() };
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let result = GroupBy::new(request.clone(), &collection, collection_by_name, hw_acc)
|
|
.execute()
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
// minimal assertion
|
|
assert_eq!(result.len(), request.limit);
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), request.group_size);
|
|
assert!(group.lookup.is_none());
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn group_by_with_lookup() {
|
|
let Resources {
|
|
mut request,
|
|
collection,
|
|
lookup_collection,
|
|
..
|
|
} = setup(16, 8).await;
|
|
|
|
request.with_lookup = Some(WithLookup {
|
|
collection_name: "test".to_string(),
|
|
with_payload: Some(true.into()),
|
|
with_vectors: Some(true.into()),
|
|
});
|
|
|
|
let collection_by_name = |_: String| async { Some(lookup_collection.clone()) };
|
|
|
|
let hw_acc = HwMeasurementAcc::new();
|
|
let result = GroupBy::new(request.clone(), &collection, collection_by_name, hw_acc)
|
|
.execute()
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
let result = result.unwrap();
|
|
|
|
assert_eq!(result.len(), request.limit);
|
|
|
|
for group in result {
|
|
assert_eq!(group.hits.len(), request.group_size);
|
|
|
|
let lookup = group.lookup.expect("lookup not found");
|
|
|
|
assert_eq!(PseudoId::from(group.id), PseudoId::from(lookup.id));
|
|
|
|
let payload = lookup.payload.unwrap();
|
|
let body = payload.0.get("body").unwrap().as_str().unwrap();
|
|
assert_eq!(body, &format!("{} {BODY_TEXT}", lookup.id));
|
|
}
|
|
}
|
|
}
|