mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
feat(fts5): add more unit tests for search
This commit is contained in:
parent
3957d789da
commit
0ddf48c460
@ -326,4 +326,74 @@ describe("Content Search", () => {
|
||||
expect(findNoteByTitle(searchResults, "The quick brown fox jumps")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Plain Text Search Matches Attribute Values", () => {
|
||||
it("should find notes by searching for label value as plain text", () => {
|
||||
// Note has a label with value "Tolkien", searching for "Tolkien" should find it
|
||||
rootNote
|
||||
.child(note("The Hobbit").label("author", "Tolkien"))
|
||||
.child(note("Dune").label("author", "Herbert"))
|
||||
.child(note("Random Note"));
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
const searchResults = searchService.findResultsWithQuery("Tolkien", searchContext);
|
||||
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "The Hobbit")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should find notes by searching for label name as plain text", () => {
|
||||
// Note has a label named "important", searching for "important" should find it
|
||||
rootNote
|
||||
.child(note("Critical Task").label("important"))
|
||||
.child(note("Regular Task"));
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
const searchResults = searchService.findResultsWithQuery("important", searchContext);
|
||||
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Critical Task")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should find notes by searching for relation name as plain text", () => {
|
||||
const author = note("J.R.R. Tolkien");
|
||||
|
||||
rootNote
|
||||
.child(note("The Hobbit").relation("writtenBy", author.note))
|
||||
.child(note("Random Book"))
|
||||
.child(author);
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
const searchResults = searchService.findResultsWithQuery("writtenBy", searchContext);
|
||||
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "The Hobbit")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should find notes when label value contains the search term", () => {
|
||||
rootNote
|
||||
.child(note("Fantasy Book").label("genre", "Science Fiction"))
|
||||
.child(note("History Book").label("genre", "Historical"));
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
const searchResults = searchService.findResultsWithQuery("Fiction", searchContext);
|
||||
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Fantasy Book")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should combine plain text attribute search with title search", () => {
|
||||
rootNote
|
||||
.child(note("Programming Guide").label("language", "JavaScript"))
|
||||
.child(note("Programming Tutorial").label("language", "Python"))
|
||||
.child(note("Cooking Guide").label("cuisine", "Italian"));
|
||||
|
||||
const searchContext = new SearchContext();
|
||||
// Search for notes with "Guide" in title AND "JavaScript" in attributes
|
||||
const searchResults = searchService.findResultsWithQuery("Guide JavaScript", searchContext);
|
||||
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(findNoteByTitle(searchResults, "Programming Guide")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -135,8 +135,6 @@ class NoteContentFulltextExp extends Expression {
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`[FTS5-CONTENT] Found ${ftsResults.length} notes matching content search`);
|
||||
|
||||
// If we need to search protected notes, use the separate method
|
||||
if (searchProtected) {
|
||||
const protectedResults = ftsSearchService.searchProtectedNotesSync(
|
||||
@ -171,8 +169,6 @@ class NoteContentFulltextExp extends Expression {
|
||||
noteIdSet.size > 0 ? noteIdSet : undefined
|
||||
);
|
||||
|
||||
log.info(`[FTS5-ATTRIBUTES] Found ${attributeNoteIds.size} notes matching attribute search`);
|
||||
|
||||
// Add notes with matching attributes
|
||||
for (const noteId of attributeNoteIds) {
|
||||
if (becca.notes[noteId]) {
|
||||
|
||||
@ -805,8 +805,6 @@ class FTSSearchService {
|
||||
value: string;
|
||||
}>(query, params);
|
||||
|
||||
log.info(`[FTS5-ATTRIBUTES-RAW] FTS5 query returned ${results.length} raw attribute matches`);
|
||||
|
||||
// Post-filter for exact word matches when operator is "="
|
||||
if (operator === "=") {
|
||||
const matchingNoteIds = new Set<string>();
|
||||
@ -817,21 +815,15 @@ class FTSSearchService {
|
||||
const nameMatch = result.name.toLowerCase() === phrase.toLowerCase();
|
||||
const valueMatch = result.value ? this.containsExactPhrase(phrase, result.value) : false;
|
||||
|
||||
log.info(`[FTS5-ATTRIBUTES-FILTER] Checking attribute: name="${result.name}", value="${result.value}", phrase="${phrase}", nameMatch=${nameMatch}, valueMatch=${valueMatch}`);
|
||||
|
||||
if (nameMatch || valueMatch) {
|
||||
matchingNoteIds.add(result.noteId);
|
||||
}
|
||||
}
|
||||
const filterTime = Date.now() - startTime;
|
||||
log.info(`[FTS5-ATTRIBUTES-FILTERED] After post-filtering: ${matchingNoteIds.size} notes match (total time: ${filterTime}ms)`);
|
||||
return matchingNoteIds;
|
||||
}
|
||||
|
||||
// For other operators, return all matching noteIds
|
||||
const searchTime = Date.now() - startTime;
|
||||
const matchingNoteIds = new Set(results.map(r => r.noteId));
|
||||
log.info(`[FTS5-ATTRIBUTES-TIME] Attribute search completed in ${searchTime}ms, found ${matchingNoteIds.size} notes`);
|
||||
return matchingNoteIds;
|
||||
|
||||
} catch (error: any) {
|
||||
|
||||
@ -16,7 +16,9 @@ const stringComparators: Record<string, Comparator<string>> = {
|
||||
"=": (comparedValue) => (val) => {
|
||||
// For the = operator, check if the value contains the exact word or phrase
|
||||
// This is case-insensitive
|
||||
if (!val) return false;
|
||||
// Handle empty/falsy values explicitly
|
||||
if (!val && !comparedValue) return true; // Both empty means equal
|
||||
if (!val || !comparedValue) return false; // One empty, one not - not equal
|
||||
|
||||
const normalizedVal = normalizeSearchText(val);
|
||||
const normalizedCompared = normalizeSearchText(comparedValue);
|
||||
@ -33,7 +35,10 @@ const stringComparators: Record<string, Comparator<string>> = {
|
||||
},
|
||||
"!=": (comparedValue) => (val) => {
|
||||
// Negation of exact word/phrase match
|
||||
if (!val) return true;
|
||||
// Handle empty/falsy values explicitly
|
||||
if (!val && !comparedValue) return false; // Both empty means equal, so != returns false
|
||||
if (!val) return true; // val is empty but comparedValue is not, they're not equal
|
||||
if (!comparedValue) return true; // val is not empty but comparedValue is, they're not equal
|
||||
|
||||
const normalizedVal = normalizeSearchText(val);
|
||||
const normalizedCompared = normalizeSearchText(comparedValue);
|
||||
|
||||
@ -25,7 +25,7 @@ export class NoteBuilder {
|
||||
isInheritable,
|
||||
name,
|
||||
value
|
||||
}).save();
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -37,7 +37,7 @@ export class NoteBuilder {
|
||||
type: "relation",
|
||||
name,
|
||||
value: targetNote.noteId
|
||||
}).save();
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -49,7 +49,7 @@ export class NoteBuilder {
|
||||
parentNoteId: this.note.noteId,
|
||||
prefix,
|
||||
notePosition: 10
|
||||
}).save();
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -70,7 +70,7 @@ export function note(title: string, extraParams: Partial<NoteRow> = {}) {
|
||||
extraParams
|
||||
);
|
||||
|
||||
const note = new BNote(row).save();
|
||||
const note = new BNote(row);
|
||||
|
||||
return new NoteBuilder(note);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user