#search #query-language #forms #sorting #terms #white-space #user-oriented

ayumu

A small, lightweight, user-oriented query language for search forms

7 releases

new 0.3.5 Feb 17, 2025
0.3.2 Feb 17, 2025
0.2.5 Feb 15, 2025
0.1.0 Feb 14, 2025

#1064 in Database interfaces

Download history 551/week @ 2025-02-12

551 downloads per month

MIT license

32KB
787 lines

Ayumu

A small, lightweight, user-oriented query language for search forms.

-- Search 'Hayao', 'Miyazaki' keywords such that release >= 2000
-- then sort by title then year
Hayao sortby:title year>=2000 Miyazaki sortby:year asc

The syntax is designed to be fast, natural, fault tolerant and easy to write on a search textbox.

Terms are separated by whitespaces and can be either a comparison, a sort-by instruction, or a keyword (only if unrecognized as a command).

The symbols were picked based on how easy they are to reach on either a PC keyboard or a smartphone.

Parsing example

let terms: Vec<Term> = ayumu::parser::parse_query(
    r#"
        Keyword1 sortby:  column desc Keyword2 title > 4
        sortby : another_column Keyword3
    "#)
    .unwrap();

// Terms: "Keyword1", "sortby:column desc", "Keyword2", "title > 4", "sortby:another_column", "Keyword3"
println!(
    "Terms: {}",
    terms
        .iter()
        .map(|term| { format!("{:?}", term.save_repr()) })
        .collect::<Vec<_>>()
        .join(", ")
);


let compact = terms.save_repr();
// Keyword1 sortby:column desc Keyword2 title > 4 sortby:another_column Keyword3
println!("{compact}");

Compiling to SQL

For example, one use case is to produce a consistent representation that can be stored in a database as user-created filters, and then compiled into a safe SQL string.

// Sample from src/tests/sqlite.rs 
// using SQLiteWhere, which is an example of such compiler

// Specify the target columns
let mut sqlite = SQLiteWhere::new(list_string(&["title", "tags", "year"]), true);

// Specify which column to compare orphan keywords with
sqlite
    .match_keywords_with(list_string(&["title", "tags"]))
    .unwrap();

// The parser is very fault-tolerant, meaning it will  
// eat up anything and error out rarely. Most errors will be caught at the compiler level  
// (e.g. bad column name in a well-defined term).
let output = sqlite.convert("Hayao sortby:title sortby:tags rand year>=2000 Miyazaki sortby:year asc");

debug_assert_eq!(
    output,
    Ok(WhereClause {
        where_clause: "(title LIKE ? OR tags LIKE ?) AND (year >= ?)".to_string(),
        order_by: "title, tags, RANDOM(), year ASC".to_string(),
        bindings: vec![
            (
                "title".to_string(),
                Value::String("%Hayao%Miyazaki%".to_string())
            ),
            (
                "tags".to_string(),
                Value::String("%Hayao%Miyazaki%".to_string())
            ),
            ("year".to_string(), Value::Number(2000.0))
        ]
    })
);

Dependencies

~1MB
~21K SLoC