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..8fe8181f1 --- /dev/null +++ b/test/server/utils/queries/libraryItemsBookFilters.test.js @@ -0,0 +1,69 @@ +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.sync({ force: true }) + }) + + 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') + }) + + 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") + }) +})