mirror of
https://github.com/qdrant/qdrant.git
synced 2026-08-06 18:10:58 -05:00
* Minor collection optimizer cleanup * Make optimizers better aware of available vs soft deleted points * Fix incorrect deleted state on proxy segment for double delete * Rename upsert_vector to upsert_point, because we work with points * Refactor point methods for more clear and consistent naming * Replace internal_size in IdTracker with total_point_count * Keep track of vector deletion count on storage creation * Add sparse index optimizer, to optimize indexes with high deletion count * Add minimum vector count threshold to sparse index optimizer * Add sparse index optimizer test * Use consistent naming, write vector in full everywhere * Simplify vacuum optimizer a bit * Merge sparse index optimizer into vacuum optimizer * Improve update_from in segment builder by returning early * More accurately count vectors in segment optimizer * Remove random from vacuum optimizer tests to make them more reliable * Don't expose the total points in segment info, use available points * Process review feedback * Compare available vectors against indexed ones in vacuum optimizer This is much better than using the number of soft-deleted vectors when the segment was created for calculations. Not to mention that value had other problems as well. * Remove create_deleted_vector_count field, update vacuum test parameters * Potentially solve out of bound panic when building index * Review fixes: - Propagate deleted flags into payload hnsw building - Use `total` number of points for building HNSW instead of number of available points - minor refactoring of `hnsw_config` copy -> clone - Better detection of `indexed_points` in HNSW * fix assert condition * Optional named vectors optimizer reveiw 2 (#1794) * review with Ivan * fmt * remove available_vector_count from segment entry * remove total_point_count from segment entry --------- Co-authored-by: Ivan Pleshkov <pleshkov.ivan@gmail.com> * rollback changes in deleted count in proxy segment * improve vector threshold detection logic in optimized_segment_builder * style changes * fix propagate deleted points to vectors * Fix typo in method name --------- Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> Co-authored-by: Ivan Pleshkov <pleshkov.ivan@gmail.com>
213 lines
6.1 KiB
Rust
213 lines
6.1 KiB
Rust
mod fixtures;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::HashSet;
|
|
use std::iter::FromIterator;
|
|
|
|
use segment::data_types::named_vectors::NamedVectors;
|
|
use segment::data_types::vectors::DEFAULT_VECTOR_NAME;
|
|
use segment::entry::entry_point::{OperationError, SegmentEntry};
|
|
use segment::segment_constructor::load_segment;
|
|
use segment::types::{Condition, Filter, WithPayload};
|
|
use tempfile::Builder;
|
|
|
|
use crate::fixtures::segment::{build_segment_1, build_segment_3};
|
|
|
|
#[test]
|
|
fn test_point_exclusion() {
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
|
|
let segment = build_segment_1(dir.path());
|
|
|
|
assert!(segment.has_point(3.into()));
|
|
|
|
let query_vector = vec![1.0, 1.0, 1.0, 1.0];
|
|
|
|
let res = segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
None,
|
|
1,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let best_match = res.get(0).expect("Non-empty result");
|
|
assert_eq!(best_match.id, 3.into());
|
|
|
|
let ids: HashSet<_> = HashSet::from_iter([3.into()]);
|
|
|
|
let frt = Filter {
|
|
should: None,
|
|
must: None,
|
|
must_not: Some(vec![Condition::HasId(ids.into())]),
|
|
};
|
|
|
|
let res = segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&frt),
|
|
1,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let best_match = res.get(0).expect("Non-empty result");
|
|
assert_ne!(best_match.id, 3.into());
|
|
|
|
let point_ids1: Vec<_> = segment.iter_points().collect();
|
|
let point_ids2: Vec<_> = segment.iter_points().collect();
|
|
|
|
assert!(!point_ids1.is_empty());
|
|
assert!(!point_ids2.is_empty());
|
|
|
|
assert_eq!(&point_ids1, &point_ids2)
|
|
}
|
|
|
|
#[test]
|
|
fn test_named_vector_search() {
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
|
|
let segment = build_segment_3(dir.path());
|
|
|
|
assert!(segment.has_point(3.into()));
|
|
|
|
let query_vector = vec![1.0, 1.0, 1.0, 1.0];
|
|
|
|
let res = segment
|
|
.search(
|
|
"vector1",
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
None,
|
|
1,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let best_match = res.get(0).expect("Non-empty result");
|
|
assert_eq!(best_match.id, 3.into());
|
|
|
|
let ids: HashSet<_> = HashSet::from_iter([3.into()]);
|
|
|
|
let frt = Filter {
|
|
should: None,
|
|
must: None,
|
|
must_not: Some(vec![Condition::HasId(ids.into())]),
|
|
};
|
|
|
|
let res = segment
|
|
.search(
|
|
"vector1",
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
Some(&frt),
|
|
1,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let best_match = res.get(0).expect("Non-empty result");
|
|
assert_ne!(best_match.id, 3.into());
|
|
|
|
let point_ids1: Vec<_> = segment.iter_points().collect();
|
|
let point_ids2: Vec<_> = segment.iter_points().collect();
|
|
|
|
assert!(!point_ids1.is_empty());
|
|
assert!(!point_ids2.is_empty());
|
|
|
|
assert_eq!(&point_ids1, &point_ids2)
|
|
}
|
|
|
|
#[test]
|
|
fn test_missed_vector_name() {
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
let mut segment = build_segment_3(dir.path());
|
|
|
|
let exists = segment
|
|
.upsert_point(
|
|
7,
|
|
1.into(),
|
|
&NamedVectors::from([
|
|
("vector2".to_owned(), vec![10.]),
|
|
("vector3".to_owned(), vec![5., 6., 7., 8.]),
|
|
]),
|
|
)
|
|
.unwrap();
|
|
assert!(exists, "this partial vector should overwrite existing");
|
|
|
|
let exists = segment
|
|
.upsert_point(
|
|
8,
|
|
6.into(),
|
|
&NamedVectors::from([
|
|
("vector2".to_owned(), vec![10.]),
|
|
("vector3".to_owned(), vec![5., 6., 7., 8.]),
|
|
]),
|
|
)
|
|
.unwrap();
|
|
assert!(!exists, "this partial vector should not existing");
|
|
}
|
|
|
|
#[test]
|
|
fn test_vector_name_not_exists() {
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
let mut segment = build_segment_3(dir.path());
|
|
|
|
let result = segment.upsert_point(
|
|
6,
|
|
6.into(),
|
|
&NamedVectors::from([
|
|
("vector1".to_owned(), vec![5., 6., 7., 8.]),
|
|
("vector2".to_owned(), vec![10.]),
|
|
("vector3".to_owned(), vec![5., 6., 7., 8.]),
|
|
("vector4".to_owned(), vec![5., 6., 7., 8.]),
|
|
]),
|
|
);
|
|
|
|
if let Err(OperationError::VectorNameNotExists { received_name }) = result {
|
|
assert!(received_name == "vector4");
|
|
} else {
|
|
panic!("wrong upsert result")
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ordered_deletion_test() {
|
|
let dir = Builder::new().prefix("segment_dir").tempdir().unwrap();
|
|
|
|
let path = {
|
|
let mut segment = build_segment_1(dir.path());
|
|
segment.delete_point(6, 5.into()).unwrap();
|
|
segment.delete_point(6, 4.into()).unwrap();
|
|
segment.flush(true).unwrap();
|
|
segment.current_path.clone()
|
|
};
|
|
|
|
let segment = load_segment(&path).unwrap().unwrap();
|
|
let query_vector = vec![1.0, 1.0, 1.0, 1.0];
|
|
let res = segment
|
|
.search(
|
|
DEFAULT_VECTOR_NAME,
|
|
&query_vector,
|
|
&WithPayload::default(),
|
|
&false.into(),
|
|
None,
|
|
1,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let best_match = res.get(0).expect("Non-empty result");
|
|
assert_eq!(best_match.id, 3.into());
|
|
}
|
|
}
|