mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 15:04:24 +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();
|
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 we need to search protected notes, use the separate method
|
||||||
if (searchProtected) {
|
if (searchProtected) {
|
||||||
const protectedResults = ftsSearchService.searchProtectedNotesSync(
|
const protectedResults = ftsSearchService.searchProtectedNotesSync(
|
||||||
@ -171,8 +169,6 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
noteIdSet.size > 0 ? noteIdSet : undefined
|
noteIdSet.size > 0 ? noteIdSet : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
log.info(`[FTS5-ATTRIBUTES] Found ${attributeNoteIds.size} notes matching attribute search`);
|
|
||||||
|
|
||||||
// Add notes with matching attributes
|
// Add notes with matching attributes
|
||||||
for (const noteId of attributeNoteIds) {
|
for (const noteId of attributeNoteIds) {
|
||||||
if (becca.notes[noteId]) {
|
if (becca.notes[noteId]) {
|
||||||
|
|||||||
@ -805,8 +805,6 @@ class FTSSearchService {
|
|||||||
value: string;
|
value: string;
|
||||||
}>(query, params);
|
}>(query, params);
|
||||||
|
|
||||||
log.info(`[FTS5-ATTRIBUTES-RAW] FTS5 query returned ${results.length} raw attribute matches`);
|
|
||||||
|
|
||||||
// Post-filter for exact word matches when operator is "="
|
// Post-filter for exact word matches when operator is "="
|
||||||
if (operator === "=") {
|
if (operator === "=") {
|
||||||
const matchingNoteIds = new Set<string>();
|
const matchingNoteIds = new Set<string>();
|
||||||
@ -817,21 +815,15 @@ class FTSSearchService {
|
|||||||
const nameMatch = result.name.toLowerCase() === phrase.toLowerCase();
|
const nameMatch = result.name.toLowerCase() === phrase.toLowerCase();
|
||||||
const valueMatch = result.value ? this.containsExactPhrase(phrase, result.value) : false;
|
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) {
|
if (nameMatch || valueMatch) {
|
||||||
matchingNoteIds.add(result.noteId);
|
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;
|
return matchingNoteIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other operators, return all matching noteIds
|
// For other operators, return all matching noteIds
|
||||||
const searchTime = Date.now() - startTime;
|
|
||||||
const matchingNoteIds = new Set(results.map(r => r.noteId));
|
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;
|
return matchingNoteIds;
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@ -16,7 +16,9 @@ const stringComparators: Record<string, Comparator<string>> = {
|
|||||||
"=": (comparedValue) => (val) => {
|
"=": (comparedValue) => (val) => {
|
||||||
// For the = operator, check if the value contains the exact word or phrase
|
// For the = operator, check if the value contains the exact word or phrase
|
||||||
// This is case-insensitive
|
// 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 normalizedVal = normalizeSearchText(val);
|
||||||
const normalizedCompared = normalizeSearchText(comparedValue);
|
const normalizedCompared = normalizeSearchText(comparedValue);
|
||||||
@ -33,7 +35,10 @@ const stringComparators: Record<string, Comparator<string>> = {
|
|||||||
},
|
},
|
||||||
"!=": (comparedValue) => (val) => {
|
"!=": (comparedValue) => (val) => {
|
||||||
// Negation of exact word/phrase match
|
// 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 normalizedVal = normalizeSearchText(val);
|
||||||
const normalizedCompared = normalizeSearchText(comparedValue);
|
const normalizedCompared = normalizeSearchText(comparedValue);
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export class NoteBuilder {
|
|||||||
isInheritable,
|
isInheritable,
|
||||||
name,
|
name,
|
||||||
value
|
value
|
||||||
}).save();
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ export class NoteBuilder {
|
|||||||
type: "relation",
|
type: "relation",
|
||||||
name,
|
name,
|
||||||
value: targetNote.noteId
|
value: targetNote.noteId
|
||||||
}).save();
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ export class NoteBuilder {
|
|||||||
parentNoteId: this.note.noteId,
|
parentNoteId: this.note.noteId,
|
||||||
prefix,
|
prefix,
|
||||||
notePosition: 10
|
notePosition: 10
|
||||||
}).save();
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -70,7 +70,7 @@ export function note(title: string, extraParams: Partial<NoteRow> = {}) {
|
|||||||
extraParams
|
extraParams
|
||||||
);
|
);
|
||||||
|
|
||||||
const note = new BNote(row).save();
|
const note = new BNote(row);
|
||||||
|
|
||||||
return new NoteBuilder(note);
|
return new NoteBuilder(note);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user