geary/src/engine/api/geary-search-query.vala
Michael Gratton 0112c8192c Geary.SearchQuery: Allow client apps to build search queries
Adds classes that allow building arbitrary query expressions and require
an instance to be provided to Geary.SearchQuery, to be set as a
property.

This enables query expressions to be parsed by clients instead of the
engine, in which ever whay they choose.
2021-01-19 20:48:17 +11:00

335 lines
9.8 KiB
Vala

/*
* Copyright © 2016 Software Freedom Conservancy Inc.
* Copyright © 2019-2020 Michael Gratton <mike@vee.met>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Specifies an expression for searching email in a search folder.
*
* New instances can be constructed via {@link
* Account.new_search_query} and then passed to search methods on
* {@link Account} or {@link App.SearchFolder}.
*
* Actual search queries are specified by the given {@link
* expression}, which is a list of {@link Term}. The expression
* denotes the conjunction of all given terms, that is, each term is
* combined by a Boolean AND function. While the order of the terms is
* not important, the expression should attempt to reflect the
* free-text search query it was built from (if any). A more
* expressive language is not supported since it is designed to work
* with both the Engine's built-in full text search system as well as
* other server-based systems, including IMAP.
*
* @see Account.new_search_query
* @see Account.local_search_async
* @see Account.get_search_matches_async
* @see App.SearchFolder.search
*/
public abstract class Geary.SearchQuery : BaseObject {
/**
* An advisory parameter regarding search quality, scope, and breadth.
*
* The Engine can perform searches based on (unspecified, uncontracted) textual variations of
* a query's search terms. Some of those variations may produce undesirable results due to
* "greedy" matching of terms. The Strategy parameter allows for an advisory to the Engine
* about how to use those textual variants, if any at all.
*
* This may be respected or ignored by the Engine. In particular, there's no guarantee it will
* have any effect on server search.
*/
public enum Strategy {
/**
* Only return exact matches, perform no searches for textual variants.
*
* Note that Geary's search syntax does prefix-matching for unquoted strings. EXACT means
* exact ''prefix-''matching in this case.
*/
EXACT,
/**
* Allow for searching for a small set of textual variants and small differences in search
* terms. This is a good default.
*/
CONSERVATIVE,
/**
* Allow for searching for a broad set of textual variants and larger differences in
* search terms.
*/
AGGRESSIVE,
/**
* Search for all textual variants, i.e. "the sky's the limit."
*/
HORIZON;
/** Determines if stemming may be used for an operator. */
internal bool is_stemming_enabled() {
return this != EXACT;
}
/**
* The minimum term length before stemming is allowed.
*
* This prevents short words that might be stemmed from being stemmed.
*/
internal int get_min_term_length_for_stemming() {
var min = 0;
switch (this) {
case EXACT:
min = int.MAX;
break;
case CONSERVATIVE:
min = 6;
break;
case AGGRESSIVE:
min = 4;
break;
case HORIZON:
min = 0;
break;
}
return min;
}
/**
* Maximum difference in lengths between term and stemmed variant.
*
* This prevents long words from being stemmed to much shorter
* words (which creates opportunities for greedy matching).
*/
internal int get_max_difference_term_stem_lengths() {
var max = 0;
switch (this) {
case EXACT:
max = 0;
break;
case CONSERVATIVE:
max = 2;
break;
case AGGRESSIVE:
max = 4;
break;
case HORIZON:
max =int.MAX;
break;
}
return max;
}
/**
* Maximum difference in lengths between a matched word and the stemmed variant it matched
* against.
*
* This prevents long words being matched to short stem
* variants (which creates opportunities for greedy matching).
*/
internal int get_max_difference_match_stem_lengths() {
var max = 0;
switch (this) {
case EXACT:
max = 0;
break;
case CONSERVATIVE:
max = 2;
break;
case AGGRESSIVE:
max = 3;
break;
case HORIZON:
max = int.MAX;
break;
}
return max;
}
}
/**
* Parent class for terms that make up a search query's expression.
*
* @see SearchQuery.expression
*/
public abstract class Term : BaseObject {
/** Determines opposite of the term is matched. */
public bool is_negated { get; set; default = false; }
/** Returns a string representation, for debugging. */
public abstract string to_string();
}
/**
* A term that matches text properties of an email.
*/
public class EmailTextTerm : Term {
/**
* Supported text email properties that can be queried.
*
* @see EmailTextTerm
*/
public enum Property {
/** Search for a term in all supported properties. */
ALL,
/** Search for a term in the To field. */
TO,
/** Search for a term in the Cc field. */
CC,
/** Search for a term in the Bcc field. */
BCC,
/** Search for a term in the From field. */
FROM,
/** Search for a term in the email subject. */
SUBJECT,
/** Search for a term in the email body. */
BODY,
/** Search for a term in email attachment names. */
ATTACHMENT_NAME;
}
/** The email property this term applies to. */
public Property target { get; private set; }
/** The strategy used for matching the given terms. */
public Strategy matching_strategy { get; private set; }
/**
* The strings to match against the given target.
*
* If more than one term is given, they are treated as the
* disjunction of all, that is they are combined using the
* Boolean OR function.
*/
public Gee.List<string> terms {
get; private set; default = new Gee.ArrayList<string>();
}
public EmailTextTerm(Property target,
Strategy matching_strategy,
string term) {
this.target = target;
this.matching_strategy = matching_strategy;
this.terms.add(term);
}
public EmailTextTerm.disjunction(Property target,
Strategy matching_strategy,
Gee.List<string> terms) {
this.target = target;
this.matching_strategy = matching_strategy;
this.terms.add_all(terms);
}
public override string to_string() {
var builder = new GLib.StringBuilder();
if (this.is_negated) {
builder.append_c('!');
}
builder.append(
ObjectUtils.to_enum_nick(
typeof(Property), this.target).up()
);
builder.append_c(':');
builder.append(
ObjectUtils.to_enum_nick(
typeof(Strategy), this.matching_strategy
).up()
);
builder.append_c('(');
var iter = this.terms.iterator();
if (iter.next()) {
builder.append(iter.get().to_string());
}
while (iter.next()) {
builder.append_c(',');
builder.append(iter.get().to_string());
}
builder.append_c(')');
return builder.str;
}
}
/**
* A term that matches a given flag in an email.
*/
public class EmailFlagTerm : Term {
public NamedFlag value { get; private set; }
public EmailFlagTerm(NamedFlag value) {
this.value = value;
}
public override string to_string() {
return "%s(%s)".printf(
this.is_negated ? "!" : "",
this.value.to_string()
);
}
}
/**
* A read-only list of search terms to be evaluated.
*
* Each given term is used in a conjunction, that is combined
* using a Boolean `AND` operator.
*/
public Gee.List<Term> expression { get; private set; }
private Gee.List<Term> _rw_expression = new Gee.ArrayList<Term>();
/**
* The original search text, if any.
*
* This is used mostly for debugging.
*/
public string raw { get; private set; }
protected SearchQuery(Gee.Collection<Term> expression,
string raw) {
this._rw_expression.add_all(expression);
this.expression = this._rw_expression.read_only_view;
this.raw = raw;
}
public string to_string() {
var builder = new GLib.StringBuilder();
builder.append_printf("\"%s\": ", this.raw);
var iter = this.expression.iterator();
if (iter.next()) {
builder.append(iter.get().to_string());
}
while (iter.next()) {
builder.append_c(',');
builder.append(iter.get().to_string());
}
return builder.str;
}
}