PostgreSQL search: find words inside URLs and file names in body text
After #37, address fields on PostgreSQL are split into words as the built-in index splits them, but language text (subject, body, attachments) still goes straight to PostgreSQL's parser, which keeps a URL, host, path or file name as tokens of its own: "https://x.example/shipping-support/" becomes a url, a host and a url_path, "invoice-2024.pdf" a file. So TEXT/BODY "shipping" missed messages where the word appears only inside a link, while RocksDB and the other built-in backends found them: 8 messages across a handful of searches in the rehearsal. On insert, language text is now indexed as it was, followed by the word parts of each token that holds a URL separator (/ . @ : ? = & # _ % + ~ \), split with SpaceTokenizer as keyword_terms() splits addresses. The parts go through the same text search configuration as the rest of the text, so they are stemmed like the words around them. Plain words, words that only carry punctuation ("end.", "(see") and hyphenated words (the parser already splits those) add nothing, so text without links is indexed exactly as before. Each part is added once per document. On sample mail, the text vector of a short order notice with three links grows from 546 to 716 bytes, a newsletter with 25 tracking links from 5586 to 6430, and a plain letter not at all. On search, a query word written as a URL, host, file or hyphenated word also matches as its word parts, ORed with the query as written, so "shipping-support" or "invoice-2024.pdf" match the new parts and documents indexed before this change still match as they did. Existing messages keep their old vectors until they are reindexed (the reindexAccounts task); new and reindexed messages match at once. store::search_tests gains test_url_word_search: five bodies, 19 body searches for words found only in a URL path, query string, host or file name, the tokens as written, plain words and non-matches, with the same expected ids on every backend. It passes on RocksDB, SQLite, MySQL and PostgreSQL; on main PostgreSQL fails at the first ("shipping" finds [3], not [0, 3]). On PostgreSQL the suite then stops at the account sort assertion (query.rs:689) exactly as it does on main.
This commit is contained in:
@@ -133,6 +133,11 @@ pub async fn test(test: &TestServer) {
|
||||
println!("Running address search tests...");
|
||||
test_address_search(store.clone()).await;
|
||||
|
||||
// inbuxa: words inside URLs, host names and file names in body text
|
||||
// are found on every backend
|
||||
println!("Running URL word search tests...");
|
||||
test_url_word_search(store.clone()).await;
|
||||
|
||||
// Large document insert test
|
||||
println!("Running large document insert tests...");
|
||||
let mut large_text = String::with_capacity(20 * 1024 * 1024);
|
||||
@@ -1129,3 +1134,79 @@ async fn test_address_search(store: SearchStore) {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn test_url_word_search(store: SearchStore) {
|
||||
const ACCOUNT_ID: u32 = 8;
|
||||
let bodies = [
|
||||
"Track your parcel here: https://x.example/shipping-support/ and reply.",
|
||||
"Reset it at https://mail.example.com/login/?password=reset&user=jane now.",
|
||||
"Attached is invoice-2024.pdf for your records.",
|
||||
"Shipping was fast, thanks again.",
|
||||
"Nothing to see at www.example.org/about-us, really.",
|
||||
];
|
||||
|
||||
let mut documents = Vec::new();
|
||||
let mut mask = RoaringBitmap::new();
|
||||
for (document_id, body) in bodies.iter().enumerate() {
|
||||
let mut document = IndexDocument::new(SearchIndex::Email)
|
||||
.with_account_id(ACCOUNT_ID)
|
||||
.with_document_id(document_id as u32);
|
||||
document.index_text(EmailSearchField::Body, body, Language::English);
|
||||
document.index_unsigned(EmailSearchField::ReceivedAt, document_id as u64);
|
||||
documents.push(document);
|
||||
mask.insert(document_id as u32);
|
||||
}
|
||||
store.index(documents).await.unwrap();
|
||||
if let SearchStore::ElasticSearch(store) = &store {
|
||||
store.refresh_index(SearchIndex::Email).await.unwrap();
|
||||
}
|
||||
|
||||
for (text, expected) in [
|
||||
// only inside a URL path, a query string or a file name
|
||||
("shipping", vec![0u32, 3]),
|
||||
("support", vec![0]),
|
||||
("password", vec![1]),
|
||||
("login", vec![1]),
|
||||
("jane", vec![1]),
|
||||
("invoice", vec![2]),
|
||||
("pdf", vec![2]),
|
||||
("2024", vec![2]),
|
||||
// host names
|
||||
("example", vec![0, 1, 4]),
|
||||
("mail", vec![1]),
|
||||
// written as they appear
|
||||
("https://x.example/shipping-support/", vec![0]),
|
||||
("shipping-support", vec![0]),
|
||||
("invoice-2024.pdf", vec![2]),
|
||||
("mail.example.com", vec![1]),
|
||||
// plain words are unaffected
|
||||
("parcel", vec![0]),
|
||||
("records", vec![2]),
|
||||
("thanks", vec![3]),
|
||||
// no match
|
||||
("billing", vec![]),
|
||||
("example.net", vec![]),
|
||||
] {
|
||||
let ids = store
|
||||
.query_account(
|
||||
SearchQuery::new(SearchIndex::Email)
|
||||
.with_filters(vec![
|
||||
SearchFilter::eq(SearchField::AccountId, ACCOUNT_ID),
|
||||
SearchFilter::has_english_text(EmailSearchField::Body, text),
|
||||
])
|
||||
.with_comparator(SearchComparator::ascending(EmailSearchField::ReceivedAt))
|
||||
.with_mask(mask.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(ids, expected, "Body {text:?}");
|
||||
}
|
||||
|
||||
store
|
||||
.unindex(
|
||||
SearchQuery::new(SearchIndex::Email)
|
||||
.with_filter(SearchFilter::eq(SearchField::AccountId, ACCOUNT_ID)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user