[backend/core] Add in:interactions search query filter, allow multiple from: filters
This commit is contained in:
parent
af5e5752d5
commit
1d02bd7119
4 changed files with 49 additions and 9 deletions
|
@ -4,6 +4,25 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
public static class ExpressionExtensions
|
public static class ExpressionExtensions
|
||||||
{
|
{
|
||||||
|
public static Expression<Func<T, bool>> True<T>() => f => true;
|
||||||
|
public static Expression<Func<T, bool>> False<T>() => f => false;
|
||||||
|
|
||||||
|
public static Expression<Func<T, bool>> Or<T>(
|
||||||
|
this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
|
||||||
|
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression<Func<T, bool>> And<T>(
|
||||||
|
this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
|
||||||
|
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
public static Expression<Func<TFirstParam, TResult>> Compose<TFirstParam, TIntermediate, TResult>(
|
public static Expression<Func<TFirstParam, TResult>> Compose<TFirstParam, TIntermediate, TResult>(
|
||||||
this Expression<Func<TFirstParam, TIntermediate>> first,
|
this Expression<Func<TFirstParam, TIntermediate>> first,
|
||||||
Expression<Func<TIntermediate, TResult>> second
|
Expression<Func<TIntermediate, TResult>> second
|
||||||
|
|
|
@ -21,14 +21,16 @@ public static class QueryableFtsExtensions
|
||||||
var caseSensitivity = parsed.OfType<CaseFilter>().LastOrDefault()?.Value ?? CaseFilterType.Insensitive;
|
var caseSensitivity = parsed.OfType<CaseFilter>().LastOrDefault()?.Value ?? CaseFilterType.Insensitive;
|
||||||
var matchType = parsed.OfType<MatchFilter>().LastOrDefault()?.Value ?? MatchFilterType.Substring;
|
var matchType = parsed.OfType<MatchFilter>().LastOrDefault()?.Value ?? MatchFilterType.Substring;
|
||||||
|
|
||||||
|
query = query.ApplyFromFilters(parsed.OfType<FromFilter>().ToList(), config, db);
|
||||||
|
|
||||||
return parsed.Aggregate(query, (current, filter) => filter switch
|
return parsed.Aggregate(query, (current, filter) => filter switch
|
||||||
{
|
{
|
||||||
CaseFilter => current,
|
CaseFilter => current,
|
||||||
MatchFilter => current,
|
MatchFilter => current,
|
||||||
|
FromFilter => current,
|
||||||
AfterFilter afterFilter => current.ApplyAfterFilter(afterFilter),
|
AfterFilter afterFilter => current.ApplyAfterFilter(afterFilter),
|
||||||
AttachmentFilter attachmentFilter => current.ApplyAttachmentFilter(attachmentFilter),
|
AttachmentFilter attachmentFilter => current.ApplyAttachmentFilter(attachmentFilter),
|
||||||
BeforeFilter beforeFilter => current.ApplyBeforeFilter(beforeFilter),
|
BeforeFilter beforeFilter => current.ApplyBeforeFilter(beforeFilter),
|
||||||
FromFilter fromFilter => current.ApplyFromFilter(fromFilter, config, db),
|
|
||||||
InFilter inFilter => current.ApplyInFilter(inFilter, user, db),
|
InFilter inFilter => current.ApplyInFilter(inFilter, user, db),
|
||||||
InstanceFilter instanceFilter => current.ApplyInstanceFilter(instanceFilter, config),
|
InstanceFilter instanceFilter => current.ApplyInstanceFilter(instanceFilter, config),
|
||||||
MentionFilter mentionFilter => current.ApplyMentionFilter(mentionFilter, config, db),
|
MentionFilter mentionFilter => current.ApplyMentionFilter(mentionFilter, config, db),
|
||||||
|
@ -75,10 +77,16 @@ public static class QueryableFtsExtensions
|
||||||
this IQueryable<Note> query, MultiWordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType
|
this IQueryable<Note> query, MultiWordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType
|
||||||
) => query.Where(p => p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType));
|
) => query.Where(p => p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType));
|
||||||
|
|
||||||
[Projectable]
|
private static IQueryable<Note> ApplyFromFilters(
|
||||||
private static IQueryable<Note> ApplyFromFilter(
|
this IQueryable<Note> query, List<FromFilter> filters, Config.InstanceSection config, DatabaseContext db
|
||||||
this IQueryable<Note> query, FromFilter filter, Config.InstanceSection config, DatabaseContext db
|
)
|
||||||
) => query.Where(p => p.User.UserSubqueryMatches(filter.Value, filter.Negated, config, db));
|
{
|
||||||
|
if (filters.Count == 0) return query;
|
||||||
|
var expr = ExpressionExtensions.False<Note>();
|
||||||
|
expr = filters.Aggregate(expr, (current, filter) => current
|
||||||
|
.Or(p => p.User.UserSubqueryMatches(filter.Value, filter.Negated, config, db)));
|
||||||
|
return query.Where(expr);
|
||||||
|
}
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
private static IQueryable<Note> ApplyInstanceFilter(
|
private static IQueryable<Note> ApplyInstanceFilter(
|
||||||
|
@ -105,7 +113,9 @@ public static class QueryableFtsExtensions
|
||||||
? query.ApplyInBookmarksFilter(user, filter.Negated, db)
|
? query.ApplyInBookmarksFilter(user, filter.Negated, db)
|
||||||
: filter.Value.Equals(InFilterType.Likes)
|
: filter.Value.Equals(InFilterType.Likes)
|
||||||
? query.ApplyInLikesFilter(user, filter.Negated, db)
|
? query.ApplyInLikesFilter(user, filter.Negated, db)
|
||||||
: query.ApplyInReactionsFilter(user, filter.Negated, db);
|
: filter.Value.Equals(InFilterType.Reactions)
|
||||||
|
? query.ApplyInReactionsFilter(user, filter.Negated, db)
|
||||||
|
: query.ApplyInInteractionsFilter(user, filter.Negated, db);
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||||
|
@ -131,6 +141,14 @@ public static class QueryableFtsExtensions
|
||||||
? !db.Users.First(u => u == user).HasReacted(p)
|
? !db.Users.First(u => u == user).HasReacted(p)
|
||||||
: db.Users.First(u => u == user).HasReacted(p));
|
: db.Users.First(u => u == user).HasReacted(p));
|
||||||
|
|
||||||
|
[Projectable]
|
||||||
|
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
|
||||||
|
internal static IQueryable<Note> ApplyInInteractionsFilter(
|
||||||
|
this IQueryable<Note> query, User user, bool negated, DatabaseContext db
|
||||||
|
) => query.Where(p => negated
|
||||||
|
? !db.Users.First(u => u == user).HasInteractedWith(p)
|
||||||
|
: db.Users.First(u => u == user).HasInteractedWith(p));
|
||||||
|
|
||||||
[Projectable]
|
[Projectable]
|
||||||
private static IQueryable<Note> ApplyMiscFilter(
|
private static IQueryable<Note> ApplyMiscFilter(
|
||||||
this IQueryable<Note> query, MiscFilter filter, User user
|
this IQueryable<Note> query, MiscFilter filter, User user
|
||||||
|
|
|
@ -63,6 +63,7 @@ module SearchQueryFilters =
|
||||||
| Bookmarks
|
| Bookmarks
|
||||||
| Likes
|
| Likes
|
||||||
| Reactions
|
| Reactions
|
||||||
|
| Interactions
|
||||||
|
|
||||||
type InFilter(neg: bool, value: string) =
|
type InFilter(neg: bool, value: string) =
|
||||||
inherit Filter()
|
inherit Filter()
|
||||||
|
@ -75,6 +76,7 @@ module SearchQueryFilters =
|
||||||
| "favorites" -> Likes
|
| "favorites" -> Likes
|
||||||
| "favourites" -> Likes
|
| "favourites" -> Likes
|
||||||
| "reactions" -> Reactions
|
| "reactions" -> Reactions
|
||||||
|
| "interactions" -> Interactions
|
||||||
| _ -> failwith $"Invalid type: {value}"
|
| _ -> failwith $"Invalid type: {value}"
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,7 +206,7 @@ module private SearchQueryParser =
|
||||||
<| fun n v -> MiscFilter(n.IsSome, v) :> Filter
|
<| fun n v -> MiscFilter(n.IsSome, v) :> Filter
|
||||||
|
|
||||||
let inFilter =
|
let inFilter =
|
||||||
negKeyFilter [ "in" ] [ "bookmarks"; "favorites"; "favourites"; "reactions"; "likes" ]
|
negKeyFilter [ "in" ] [ "bookmarks"; "favorites"; "favourites"; "reactions"; "likes"; "interactions" ]
|
||||||
<| fun n v -> InFilter(n.IsSome, v) :> Filter
|
<| fun n v -> InFilter(n.IsSome, v) :> Filter
|
||||||
|
|
||||||
let attachmentFilter =
|
let attachmentFilter =
|
||||||
|
|
|
@ -138,7 +138,7 @@ public class SearchQueryTests
|
||||||
public void TestParseIn(bool negated)
|
public void TestParseIn(bool negated)
|
||||||
{
|
{
|
||||||
var key = negated ? "-in" : "in";
|
var key = negated ? "-in" : "in";
|
||||||
List<string> candidates = ["bookmarks", "likes", "favorites", "favourites", "reactions"];
|
List<string> candidates = ["bookmarks", "likes", "favorites", "favourites", "reactions", "interactions"];
|
||||||
var results = candidates.Select(v => $"{key}:{v}").SelectMany(SearchQuery.parse).ToList();
|
var results = candidates.Select(v => $"{key}:{v}").SelectMany(SearchQuery.parse).ToList();
|
||||||
List<Filter> expectedResults =
|
List<Filter> expectedResults =
|
||||||
[
|
[
|
||||||
|
@ -146,7 +146,8 @@ public class SearchQueryTests
|
||||||
new InFilter(negated, "likes"),
|
new InFilter(negated, "likes"),
|
||||||
new InFilter(negated, "likes"),
|
new InFilter(negated, "likes"),
|
||||||
new InFilter(negated, "likes"),
|
new InFilter(negated, "likes"),
|
||||||
new InFilter(negated, "reactions")
|
new InFilter(negated, "reactions"),
|
||||||
|
new InFilter(negated, "interactions")
|
||||||
];
|
];
|
||||||
results.Should()
|
results.Should()
|
||||||
.HaveCount(expectedResults.Count)
|
.HaveCount(expectedResults.Count)
|
||||||
|
|
Loading…
Add table
Reference in a new issue