From 9491db51de273768fa48e23167d43a3e43a0f3b7 Mon Sep 17 00:00:00 2001 From: leahjessie <740226+leahjessie@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:05:18 -0700 Subject: [PATCH 1/2] Fix punctuation-insensitive library search --- server/Database.js | 22 ++++++++-- .../queries/libraryItemsBookFilters.test.js | 44 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 test/server/utils/queries/libraryItemsBookFilters.test.js diff --git a/server/Database.js b/server/Database.js index 213c2c61b..7a0f342d5 100644 --- a/server/Database.js +++ b/server/Database.js @@ -966,6 +966,20 @@ WHERE EXISTS ( return `unaccent(${value})` } + /** + * Returns an expression that strips punctuation the older in-memory search ignored. + * + * @param {string} expression + * @returns {string} + */ + stripPunctuation(expression) { + const punctuationChars = ["'", '.', '`', '"', ','] + punctuationChars.forEach((char) => { + expression = `replace(${expression}, ${this.sequelize.escape(char)}, '')` + }) + return expression + } + /** * Initialize the text query. * @@ -983,15 +997,17 @@ WHERE EXISTS ( * Get match expression for the specified column. * If the query contains accents, match against the column as-is (case-insensitive exact match). * otherwise match against a normalized column (case-insensitive match with accents removed). + * Punctuation is stripped from both sides in all cases. * * @param {string} column * @returns {string} */ matchExpression(column) { - const pattern = this.sequelize.escape(`%${this.query}%`) - if (!this.supportsUnaccent) return `${column} LIKE ${pattern}` + const queryExpr = this.stripPunctuation(this.sequelize.escape(this.query)) + const pattern = `'%' || ${queryExpr} || '%'` + if (!this.supportsUnaccent) return `${this.stripPunctuation(column)} LIKE ${pattern}` const normalizedColumn = this.hasAccents ? column : this.normalize(column) - return `${normalizedColumn} LIKE ${pattern}` + return `${this.stripPunctuation(normalizedColumn)} LIKE ${pattern}` } } } diff --git a/test/server/utils/queries/libraryItemsBookFilters.test.js b/test/server/utils/queries/libraryItemsBookFilters.test.js new file mode 100644 index 000000000..a795c031d --- /dev/null +++ b/test/server/utils/queries/libraryItemsBookFilters.test.js @@ -0,0 +1,44 @@ +const { expect } = require('chai') +const { Sequelize } = require('sequelize') + +const Database = require('../../../../server/Database') +const libraryItemsBookFilters = require('../../../../server/utils/queries/libraryItemsBookFilters') + +describe('libraryItemsBookFilters.search', () => { + beforeEach(async () => { + global.ServerSettings = {} + Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + Database.supportsUnaccent = false + await Database.buildModels() + }) + + afterEach(async () => { + await Database.sequelize.close() + }) + + it('matches titles when the query omits commas', async () => { + const library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + const libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + const book = await Database.bookModel.create({ + title: 'And Now, Back to You', + audioFiles: [], + tags: [], + narrators: [], + genres: [], + chapters: [] + }) + await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book.id, + mediaType: 'book', + libraryId: library.id, + libraryFolderId: libraryFolder.id + }) + + const results = await libraryItemsBookFilters.search(null, library, 'And Now Back to You', 10, 0) + + expect(results.book).to.have.length(1) + expect(results.book[0].libraryItem.media.metadata.title).to.equal('And Now, Back to You') + }) +}) From c195550209d4c5f0ab57e1a826fa7aaabb7cb581 Mon Sep 17 00:00:00 2001 From: leahjessie <740226+leahjessie@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:02:25 -0700 Subject: [PATCH 2/2] Add punctuation normalization regression tests --- .../queries/libraryItemsBookFilters.test.js | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/server/utils/queries/libraryItemsBookFilters.test.js b/test/server/utils/queries/libraryItemsBookFilters.test.js index a795c031d..8fe8181f1 100644 --- a/test/server/utils/queries/libraryItemsBookFilters.test.js +++ b/test/server/utils/queries/libraryItemsBookFilters.test.js @@ -14,7 +14,7 @@ describe('libraryItemsBookFilters.search', () => { }) afterEach(async () => { - await Database.sequelize.close() + await Database.sequelize.sync({ force: true }) }) it('matches titles when the query omits commas', async () => { @@ -41,4 +41,29 @@ describe('libraryItemsBookFilters.search', () => { expect(results.book).to.have.length(1) expect(results.book[0].libraryItem.media.metadata.title).to.equal('And Now, Back to You') }) + + it('matches titles when the query omits apostrophes', async () => { + const library = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + const libraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: library.id }) + const book = await Database.bookModel.create({ + title: "Don't Panic", + audioFiles: [], + tags: [], + narrators: [], + genres: [], + chapters: [] + }) + await Database.libraryItemModel.create({ + libraryFiles: [], + mediaId: book.id, + mediaType: 'book', + libraryId: library.id, + libraryFolderId: libraryFolder.id + }) + + const results = await libraryItemsBookFilters.search(null, library, 'Dont Panic', 10, 0) + + expect(results.book).to.have.length(1) + expect(results.book[0].libraryItem.media.metadata.title).to.equal("Don't Panic") + }) })