search: add localOnly to web app queries, fix string+bool queries

localOnly: true in a queryType means it won't be sent to the server, but
will be evaluated by the web app on the returned search results.
This commit is contained in:
Harvey Tindall
2025-05-26 16:06:41 +01:00
parent 9715f90a48
commit ef253de56b
4 changed files with 104 additions and 32 deletions
+10 -3
View File
@@ -43,7 +43,6 @@ func activityDTONameToField(field string) string {
return "IP" return "IP"
} }
return "unknown" return "unknown"
// Only these query types actually search the ActivityDTO data.
} }
func activityTypeGetterNameToType(getter string) ActivityType { func activityTypeGetterNameToType(getter string) ActivityType {
@@ -221,10 +220,18 @@ func matchReferrerAsQuery(jf *mediabrowser.MediaBrowser, query *badgerhold.Query
criterion := andField(query, "Type") criterion := andField(query, "Type")
query = criterion.MatchFunc(func(ra *badgerhold.RecordAccess) (bool, error) { query = criterion.MatchFunc(func(ra *badgerhold.RecordAccess) (bool, error) {
act := ra.Record().(*Activity) act := ra.Record().(*Activity)
if act.Type == ActivityCreation || act.SourceType == ActivityUser || !act.SourceIsUser() { if act.Type != ActivityCreation || act.SourceType != ActivityUser || !act.SourceIsUser() {
return false, nil return false, nil
} }
return strings.Contains(strings.ToLower(act.MustGetSourceUsername(jf)), strings.ToLower(q.Value.(string))), nil sourceUsername := act.MustGetSourceUsername(jf)
if q.Class == BoolQuery {
val := sourceUsername != ""
if q.Value.(bool) == false {
val = !val
}
return val, nil
}
return strings.Contains(strings.ToLower(sourceUsername), strings.ToLower(q.Value.(string))), nil
}) })
return query return query
} }
+2 -1
View File
@@ -50,7 +50,8 @@ const queries = (): { [field: string]: QueryType } => { return {
getter: "title", getter: "title",
bool: false, bool: false,
string: true, string: true,
date: false date: false,
localOnly: true
}, },
"user": { "user": {
name: window.lang.strings("usersMentioned"), name: window.lang.strings("usersMentioned"),
+66 -24
View File
@@ -28,6 +28,7 @@ export interface QueryType {
date: boolean; date: boolean;
dependsOnElement?: string; // Format for querySelector dependsOnElement?: string; // Format for querySelector
show?: boolean; show?: boolean;
localOnly?: boolean // Indicates can't be performed server-side.
} }
export interface SearchConfiguration { export interface SearchConfiguration {
@@ -84,7 +85,8 @@ export abstract class Query {
public abstract compare(subjectValue: any): boolean; public abstract compare(subjectValue: any): boolean;
asDTO(): QueryDTO { asDTO(): QueryDTO | null {
if (this.localOnly) return null;
let out = {} as QueryDTO; let out = {} as QueryDTO;
out.field = this._subject.getter; out.field = this._subject.getter;
out.operator = this._operator; out.operator = this._operator;
@@ -100,6 +102,8 @@ export abstract class Query {
compareItem(item: SearchableItem): boolean { compareItem(item: SearchableItem): boolean {
return this.compare(this.getValueFromItem(item)); return this.compare(this.getValueFromItem(item));
} }
get localOnly(): boolean { return this._subject.localOnly ? true : false; }
} }
export class BoolQuery extends Query { export class BoolQuery extends Query {
@@ -134,8 +138,9 @@ export class BoolQuery extends Query {
return ((subjectBool && this._value) || (!subjectBool && !this._value)) return ((subjectBool && this._value) || (!subjectBool && !this._value))
} }
asDTO(): QueryDTO { asDTO(): QueryDTO | null {
let out = super.asDTO(); let out = super.asDTO();
if (out === null) return null;
out.class = "bool"; out.class = "bool";
out.value = this._value; out.value = this._value;
return out; return out;
@@ -159,8 +164,9 @@ export class StringQuery extends Query {
return subjectString.toLowerCase().includes(this._value); return subjectString.toLowerCase().includes(this._value);
} }
asDTO(): QueryDTO { asDTO(): QueryDTO | null {
let out = super.asDTO(); let out = super.asDTO();
if (out === null) return null;
out.class = "string"; out.class = "string";
out.value = this._value; out.value = this._value;
return out; return out;
@@ -259,8 +265,9 @@ export class DateQuery extends Query {
return subjectDate > temp; return subjectDate > temp;
} }
asDTO(): QueryDTO { asDTO(): QueryDTO | null {
let out = super.asDTO(); let out = super.asDTO();
if (out === null) return null;
out.class = "date"; out.class = "date";
out.value = this._value.attempt; out.value = this._value.attempt;
return out; return out;
@@ -372,6 +379,8 @@ export class Search {
} }
this._c.search.oninput((null as Event)); this._c.search.oninput((null as Event));
}; };
queries.push(q);
continue;
} }
} }
if (queryFormat.string) { if (queryFormat.string) {
@@ -384,6 +393,8 @@ export class Search {
} }
this._c.search.oninput((null as Event)); this._c.search.oninput((null as Event));
} }
queries.push(q);
continue;
} }
if (queryFormat.date) { if (queryFormat.date) {
let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]); let [parsedDate, op, isDate] = DateQuery.paramsFromString(split[1]);
@@ -398,37 +409,48 @@ export class Search {
this._c.search.oninput((null as Event)); this._c.search.oninput((null as Event));
} }
queries.push(q);
continue;
} }
// if (q != null) queries.push(q);
if (q != null) queries.push(q);
} }
return [searchTerms, queries]; return [searchTerms, queries];
} }
// Returns a list of identifiers (used as keys in items, values in ordering). // Returns a list of identifiers (used as keys in items, values in ordering).
search = (query: string): string[] => { searchParsed = (searchTerms: string[], queries: Query[]): string[] => {
let timer = this.timeSearches ? performance.now() : null;
this._c.filterArea.textContent = "";
let result: string[] = [...this._ordering]; let result: string[] = [...this._ordering];
// If we're in a server search already, the results are already correct. // If we're in a server search already, the results are (probably) already correct.
if (this.inServerSearch) return result; if (this.inServerSearch) {
let hasLocalOnlyQueries = false;
for (const q of queries) {
if (q.localOnly) {
hasLocalOnlyQueries = true;
break;
}
}
if (!hasLocalOnlyQueries) return result;
// Continue on if really necessary
}
const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query)); // Normal searches can be evaluated by the server, so skip this if we've already ran one.
if (!this.inServerSearch) {
query = ""; for (let term of searchTerms) {
let cachedResult = [...result];
for (let term of searchTerms) { for (let id of cachedResult) {
let cachedResult = [...result]; const u = this.items[id];
for (let id of cachedResult) { if (!u.matchesSearch(term)) {
const u = this.items[id]; result.splice(result.indexOf(id), 1);
if (!u.matchesSearch(term)) { }
result.splice(result.indexOf(id), 1);
} }
} }
} }
for (let q of queries) { for (let q of queries) {
this._c.filterArea.appendChild(q.asElement()); this._c.filterArea.appendChild(q.asElement());
// Skip if this query has already been performed by the server.
if (this.inServerSearch && !(q.localOnly)) continue;
let cachedResult = [...result]; let cachedResult = [...result];
if (q.subject.bool) { if (q.subject.bool) {
for (let id of cachedResult) { for (let id of cachedResult) {
@@ -466,7 +488,18 @@ export class Search {
} }
} }
} }
return result;
}
// Returns a list of identifiers (used as keys in items, values in ordering).
search = (query: string): string[] => {
let timer = this.timeSearches ? performance.now() : null;
this._c.filterArea.textContent = "";
const [searchTerms, queries] = this.parseTokens(Search.tokenizeSearch(query));
let result = this.searchParsed(searchTerms, queries);
this._queries = queries; this._queries = queries;
this._searchTerms = searchTerms; this._searchTerms = searchTerms;
@@ -476,6 +509,11 @@ export class Search {
} }
return result; return result;
} }
// postServerSearch performs local-only queries after a server search if necessary.
postServerSearch = () => {
this.searchParsed(this._searchTerms, this._queries);
};
showHideSearchOptionsHeader = () => { showHideSearchOptionsHeader = () => {
let sortingBy = false; let sortingBy = false;
@@ -642,12 +680,16 @@ export class Search {
serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => { serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => {
let req: ServerSearchReqDTO = { let req: ServerSearchReqDTO = {
searchTerms: searchTerms, searchTerms: searchTerms,
queries: queries.map((q: Query) => q.asDTO()), queries: [], // queries.map((q: Query) => q.asDTO()) won't work as localOnly queries return null
limit: -1, limit: -1,
page: 0, page: 0,
sortByField: this.sortField, sortByField: this.sortField,
ascending: this.ascending ascending: this.ascending
}; };
for (const q of queries) {
const dto = q.asDTO();
if (dto !== null) req.queries.push(dto);
}
return req; return req;
} }
+26 -4
View File
@@ -306,16 +306,38 @@ func (q QueryDTO) AsFilter() Filter {
return cmp.Compare(bool2int(a.NotifyThroughEmail), bool2int(q.Value.(bool))) == int(operator) return cmp.Compare(bool2int(a.NotifyThroughEmail), bool2int(q.Value.(bool))) == int(operator)
} }
case "last_active": case "last_active":
return func(a *respUser) bool { switch q.Class {
return q.Value.(DateAttempt).CompareUnix(a.LastActive) == int(operator) case DateQuery:
return func(a *respUser) bool {
return q.Value.(DateAttempt).CompareUnix(a.LastActive) == int(operator)
}
case BoolQuery:
return func(a *respUser) bool {
val := a.LastActive != 0
if q.Value.(bool) == false {
val = !val
}
return val
}
} }
case "admin": case "admin":
return func(a *respUser) bool { return func(a *respUser) bool {
return cmp.Compare(bool2int(a.Admin), bool2int(q.Value.(bool))) == int(operator) return cmp.Compare(bool2int(a.Admin), bool2int(q.Value.(bool))) == int(operator)
} }
case "expiry": case "expiry":
return func(a *respUser) bool { switch q.Class {
return q.Value.(DateAttempt).CompareUnix(a.Expiry) == int(operator) case DateQuery:
return func(a *respUser) bool {
return q.Value.(DateAttempt).CompareUnix(a.Expiry) == int(operator)
}
case BoolQuery:
return func(a *respUser) bool {
val := a.Expiry != 0
if q.Value.(bool) == false {
val = !val
}
return val
}
} }
case "disabled": case "disabled":
return func(a *respUser) bool { return func(a *respUser) bool {