diff --git a/crates/huginn-daemon/src/index/tests.rs b/crates/huginn-daemon/src/index/tests.rs index 831e401..855bef6 100644 --- a/crates/huginn-daemon/src/index/tests.rs +++ b/crates/huginn-daemon/src/index/tests.rs @@ -1101,16 +1101,13 @@ fn qdrant_search_scores_only_the_members_exactly_and_reads_ids_kinds_and_scores( ); let seen = answering.seen.lock().unwrap(); assert_eq!((seen[0].0.as_str(), seen[0].1.as_str()), ("POST", format!("/collections/{name}/points/search").as_str())); - for expected in [ - "\"vector\":[0.5,1.0,2.0,3.0]", - "\"has_id\":[\"p1\",\"p2\"]", - "\"exact\":true", - "\"limit\":12", - "\"with_vector\":false", - ] { - assert!(seen[0].2.contains(expected), "{expected} in {}", seen[0].2); - } - assert!(!seen[0].2.contains("\"kind\":{"), "membership is ids alone, no predicate on a payload field: {}", seen[0].2); + let body: serde_json::Value = serde_json::from_str(&seen[0].2).expect("the request body is JSON"); + assert_eq!(body["vector"], serde_json::json!([0.5, 1.0, 2.0, 3.0])); + assert_eq!(body["filter"], serde_json::json!({ "must": [{ "has_id": ["p1", "p2"] }] }), "members are included, by id alone"); + assert_eq!(body["params"], serde_json::json!({ "exact": true }), "exact search, under params"); + assert_eq!(body["limit"], 12); + assert_eq!(body["with_vector"], false); + assert!(body.get("exact").is_none(), "a top-level exact is ignored by Qdrant: {body}"); drop(seen); let nameless = stub(&[("POST /collections/", 200, r#"{"result":[{"id":"p","score":0.5,"payload":{"kind":"question"}}]}"#)]); diff --git a/crates/huginn-daemon/tests/live_index_smoke.rs b/crates/huginn-daemon/tests/live_index_smoke.rs index 1fc60c4..c6a2c9e 100644 --- a/crates/huginn-daemon/tests/live_index_smoke.rs +++ b/crates/huginn-daemon/tests/live_index_smoke.rs @@ -46,9 +46,13 @@ fn the_adapters_embed_write_query_and_clean_up_a_scratch_collection() { let mut embedder = OllamaEmbedder::new(&ollama, &model); let identity = embedder.model_identity(Duration::from_secs(60)).expect("the model is listed"); println!("identity: {identity:?}"); - let texts = ["The organ owns the mind.".to_string(), "A recipe for lentil soup.".to_string()]; - let vectors = embedder.embed(&texts, Duration::from_secs(60)).expect("two texts embed"); - assert_eq!(vectors.len(), 2); + let texts = [ + "The organ owns the mind.".to_string(), + "A recipe for lentil soup.".to_string(), + "How to keep a sourdough starter alive.".to_string(), + ]; + let vectors = embedder.embed(&texts, Duration::from_secs(60)).expect("three texts embed"); + assert_eq!(vectors.len(), 3); assert!(vectors.iter().all(|vector| vector.len() == identity.dimensions as usize)); let meta = CollectionMeta { @@ -102,6 +106,12 @@ Query: Who is responsible for keeping the memory?"); assert_eq!(hits.len(), 2, "{hits:?}"); assert_eq!((hits[0].doc_id.as_str(), hits[0].kind.as_str()), ("smoke:doc:0", "question"), "{hits:?}"); assert!(hits[0].score > hits[1].score, "{hits:?}"); + // The members are a strict subset that leaves out the nearest point: + // the filter must exclude it, not merely rank it. + let subset: Vec = points[1..].iter().map(|point| point.id.clone()).collect(); + let hits = store.search(name, &query, &subset, 3, Duration::from_secs(60)).unwrap(); + let docs: BTreeSet<&str> = hits.iter().map(|hit| hit.doc_id.as_str()).collect(); + assert_eq!(docs, BTreeSet::from(["smoke:doc:1", "smoke:doc:2"]), "only the members are scored: {hits:?}"); println!("{name}: described, created, upserted {} points, queried, searched: {hits:?}", points.len()); } }));