/* * Copyright (c) 2023, Stalwart Labs Ltd. * * This file is part of the Stalwart Mail Server. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * in the LICENSE file at the top-level directory of this distribution. * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * You can be released from the requirements of the AGPLv3 license by * purchasing a commercial license. Please contact licensing@stalw.art * for more details. */ use std::{ sync::{Arc, Mutex}, time::Instant, }; use jmap_proto::types::keyword::Keyword; use nlp::language::Language; use store::{ahash::AHashMap, query::sort::Pagination, write::ValueClass}; use store::{ query::{Comparator, Filter}, write::{BatchBuilder, F_BITMAP, F_INDEX, F_VALUE}, Store, ValueKey, }; use crate::store::deflate_artwork_data; pub const FIELDS: [&str; 20] = [ "id", "accession_number", "artist", "artistRole", "artistId", "title", "dateText", "medium", "creditLine", "year", "acquisitionYear", "dimensions", "width", "height", "depth", "units", "inscription", "thumbnailCopyright", "thumbnailUrl", "url", ]; const COLLECTION_ID: u8 = 0; enum FieldType { Keyword, Text, FullText, Integer, } const FIELDS_OPTIONS: [FieldType; 20] = [ FieldType::Integer, // "id", FieldType::Keyword, // "accession_number", FieldType::Text, // "artist", FieldType::Keyword, // "artistRole", FieldType::Integer, // "artistId", FieldType::FullText, // "title", FieldType::FullText, // "dateText", FieldType::FullText, // "medium", FieldType::FullText, // "creditLine", FieldType::Integer, // "year", FieldType::Integer, // "acquisitionYear", FieldType::FullText, // "dimensions", FieldType::Integer, // "width", FieldType::Integer, // "height", FieldType::Integer, // "depth", FieldType::Text, // "units", FieldType::FullText, // "inscription", FieldType::Text, // "thumbnailCopyright", FieldType::Text, // "thumbnailUrl", FieldType::Text, // "url", ]; #[allow(clippy::mutex_atomic)] pub async fn test(db: Store, do_insert: bool) { println!("Running Store query tests..."); let pool = rayon::ThreadPoolBuilder::new() .num_threads(8) .build() .unwrap(); let now = Instant::now(); let documents = Arc::new(Mutex::new(Vec::new())); if do_insert { pool.scope_fifo(|s| { for (document_id, record) in csv::ReaderBuilder::new() .has_headers(true) .from_reader(&deflate_artwork_data()[..]) .records() .enumerate() { let record = record.unwrap(); let documents = documents.clone(); s.spawn_fifo(move |_| { /*let mut fts_builder = FtsIndexBuilder::with_default_language(Language::English); let mut builder = BatchBuilder::new(); builder .with_account_id(0) .with_collection(COLLECTION_ID) .create_document(document_id as u32); for (pos, field) in record.iter().enumerate() { let field_id = pos as u8; match FIELDS_OPTIONS[pos] { FieldType::Text => { if !field.is_empty() { builder.value( field_id, field.to_lowercase(), F_VALUE | F_BITMAP, ); } } FieldType::FullText => { if !field.is_empty() { fts_builder.index( field_id, field.to_lowercase(), Language::English, ); if field_id == 7 { builder.value(field_id, field.to_lowercase(), F_INDEX); } } } FieldType::Integer => { builder.value( field_id, field.parse::().unwrap_or(0), F_VALUE | F_INDEX, ); } FieldType::Keyword => { if !field.is_empty() { builder.value( field_id, Keyword::Other(field.to_lowercase()), F_VALUE | F_INDEX | F_BITMAP, ); } } } } builder.custom(fts_builder); documents.lock().unwrap().push(builder.build());*/ }); } }); println!( "Parsed {} entries in {} ms.", documents.lock().unwrap().len(), now.elapsed().as_millis() ); let now = Instant::now(); let batches = documents.lock().unwrap().drain(..).collect::>(); let mut chunk = Vec::new(); for batch in batches { let chunk_instance = Instant::now(); chunk.push({ let db = db.clone(); tokio::spawn(async move { db.write(batch).await }) }); if chunk.len() == 1000 { for handle in chunk { handle.await.unwrap().unwrap(); } println!( "Chunk insert took {} ms.", chunk_instance.elapsed().as_millis() ); chunk = Vec::new(); } } if !chunk.is_empty() { for handle in chunk { handle.await.unwrap().unwrap(); } } println!("Insert took {} ms.", now.elapsed().as_millis()); } println!("Running filter tests..."); test_filter(db.clone()).await; println!("Running sort tests..."); test_sort(db).await; } pub async fn test_filter(db: Store) { /* let mut fields = AHashMap::default(); for (field_num, field) in FIELDS.iter().enumerate() { fields.insert(field.to_string(), field_num as u8); } let tests = [ ( vec![ Filter::has_english_text(fields["title"], "water"), Filter::eq(fields["year"], 1979u32), ], vec!["p11293"], ), ( vec![ Filter::has_english_text(fields["medium"], "gelatin"), Filter::gt(fields["year"], 2000u32), Filter::lt(fields["width"], 180u32), Filter::gt(fields["width"], 0u32), ], vec!["p79426", "p79427", "p79428", "p79429", "p79430"], ), ( vec![Filter::has_english_text(fields["title"], "'rustic bridge'")], vec!["d05503"], ), ( vec![ Filter::has_english_text(fields["title"], "'rustic'"), Filter::has_english_text(fields["title"], "study"), ], vec!["d00399", "d05352"], ), ( vec![ Filter::has_text(fields["artist"], "mauro kunst", Language::None), Filter::is_in_bitmap(fields["artistRole"], Keyword::Other("artist".to_string())), Filter::Or, Filter::eq(fields["year"], 1969u32), Filter::eq(fields["year"], 1971u32), Filter::End, ], vec!["p01764", "t05843"], ), ( vec![ Filter::Not, Filter::has_english_text(fields["medium"], "oil"), Filter::End, Filter::has_english_text(fields["creditLine"], "bequeath"), Filter::Or, Filter::And, Filter::ge(fields["year"], 1900u32), Filter::lt(fields["year"], 1910u32), Filter::End, Filter::And, Filter::ge(fields["year"], 2000u32), Filter::lt(fields["year"], 2010u32), Filter::End, Filter::End, ], vec![ "n02478", "n02479", "n03568", "n03658", "n04327", "n04328", "n04721", "n04739", "n05095", "n05096", "n05145", "n05157", "n05158", "n05159", "n05298", "n05303", "n06070", "t01181", "t03571", "t05805", "t05806", "t12147", "t12154", "t12155", ], ), ( vec![ Filter::And, Filter::has_text(fields["artist"], "warhol", Language::None), Filter::Not, Filter::has_english_text(fields["title"], "'campbell'"), Filter::End, Filter::Not, Filter::Or, Filter::gt(fields["year"], 1980u32), Filter::And, Filter::gt(fields["width"], 500u32), Filter::gt(fields["height"], 500u32), Filter::End, Filter::End, Filter::End, Filter::eq(fields["acquisitionYear"], 2008u32), Filter::End, ], vec!["ar00039", "t12600"], ), ( vec![ Filter::has_english_text(fields["title"], "study"), Filter::has_english_text(fields["medium"], "paper"), Filter::has_english_text(fields["creditLine"], "'purchased'"), Filter::Not, Filter::has_english_text(fields["title"], "'anatomical'"), Filter::has_english_text(fields["title"], "'for'"), Filter::End, Filter::gt(fields["year"], 1900u32), Filter::gt(fields["acquisitionYear"], 2000u32), ], vec![ "p80042", "p80043", "p80044", "p80045", "p80203", "t11937", "t12172", ], ), ]; for (filter, expected_results) in tests { //println!("Running test: {:?}", filter); let docset = db.filter(0, COLLECTION_ID, filter).await.unwrap(); let sorted_docset = db .sort( docset, vec![Comparator::ascending(fields["accession_number"])], Pagination::new(0, 0, None, 0), ) .await .unwrap(); assert_eq!( db.get_values::( sorted_docset .ids .into_iter() .map(|document_id| ValueKey { account_id: 0, collection: COLLECTION_ID, document_id: document_id as u32, family: 0, field: fields["accession_number"], }) .collect() ) .await .unwrap() .into_iter() .flatten() .collect::>(), expected_results ); } */ } pub async fn test_sort(db: Store) { let mut fields = AHashMap::default(); for (field_num, field) in FIELDS.iter().enumerate() { fields.insert(field.to_string(), field_num as u8); } let tests = [ ( vec![ Filter::gt(fields["year"], 0u32), Filter::gt(fields["acquisitionYear"], 0u32), Filter::gt(fields["width"], 0u32), ], vec![ Comparator::descending(fields["year"]), Comparator::ascending(fields["acquisitionYear"]), Comparator::ascending(fields["width"]), Comparator::descending(fields["accession_number"]), ], vec![ "t13655", "t13811", "p13352", "p13351", "p13350", "p13349", "p13348", "p13347", "p13346", "p13345", "p13344", "p13342", "p13341", "p13340", "p13339", "p13338", "p13337", "p13336", "p13335", "p13334", "p13333", "p13332", "p13331", "p13330", "p13329", "p13328", "p13327", "p13326", "p13325", "p13324", "p13323", "t13786", "p13322", "p13321", "p13320", "p13319", "p13318", "p13317", "p13316", "p13315", "p13314", "t13588", "t13587", "t13586", "t13585", "t13584", "t13540", "t13444", "ar01154", "ar01153", ], ), ( vec![ Filter::gt(fields["width"], 0u32), Filter::gt(fields["height"], 0u32), ], vec![ Comparator::descending(fields["width"]), Comparator::ascending(fields["height"]), ], vec![ "t03681", "t12601", "ar00166", "t12625", "t12915", "p04182", "t06483", "ar00703", "t07671", "ar00021", "t05557", "t07918", "p06298", "p05465", "p06640", "t12855", "t01355", "t12800", "t12557", "t02078", ], ), ( vec![], vec![ Comparator::descending(fields["medium"]), Comparator::descending(fields["artistRole"]), Comparator::ascending(fields["accession_number"]), ], vec![ "ar00627", "ar00052", "t00352", "t07275", "t12318", "t04931", "t13683", "t13686", "t13687", "t13688", "t13689", "t13690", "t13691", "t07766", "t07918", "t12993", "ar00044", "t13326", "t07614", "t12414", ], ), ]; for (filter, sort, expected_results) in tests { //println!("Running test: {:?}", sort); let docset = db.filter(0, COLLECTION_ID, filter).await.unwrap(); let sorted_docset = db .sort( docset, sort, Pagination::new(expected_results.len(), 0, None, 0), ) .await .unwrap(); assert_eq!( db.get_values::( sorted_docset .ids .into_iter() .map(|document_id| ValueKey { account_id: 0, collection: COLLECTION_ID, document_id: document_id as u32, class: ValueClass::Property(fields["accession_number"]) }) .collect() ) .await .unwrap() .into_iter() .flatten() .collect::>(), expected_results ); } }