feat(fts5): add more unit tests for search

This commit is contained in:
perfectra1n 2025-11-24 13:30:40 -08:00
parent 3957d789da
commit 0ddf48c460
5 changed files with 81 additions and 18 deletions

View File

@ -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();
});
});
}); });

View File

@ -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]) {

View File

@ -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) {

View File

@ -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);

View File

@ -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);
} }