diff --git a/src/server/loaders.js b/src/server/loaders.js index 681b631..40a18f0 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -152,14 +152,22 @@ const buildItemSearchLoader = (db, loaders) => // This isn't actually optimized as a batch query, we're just using a // DataLoader API consistency with our other loaders! const queryPromises = queries.map(async (query) => { - const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%"; + // Split the query into words, and search for each word as a substring + // of the name. + const words = query.split(/\s+/); + const wordMatchersForMysql = words.map( + (word) => "%" + word.replace(/_%/g, "\\$0") + "%" + ); + const matcherPlaceholders = words + .map((_) => "t.name LIKE ?") + .join(" AND "); const [rows, _] = await db.execute( `SELECT items.*, t.name FROM items INNER JOIN item_translations t ON t.item_id = items.id - WHERE t.name LIKE ? AND t.locale="en" + WHERE ${matcherPlaceholders} AND t.locale="en" ORDER BY t.name LIMIT 30`, - [queryForMysql] + [...wordMatchersForMysql] ); const entities = rows.map(normalizeRow); @@ -185,18 +193,24 @@ const buildItemSearchToFitLoader = (db, loaders) => const actualOffset = offset || 0; const actualLimit = Math.min(limit || 30, 30); - const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%"; + const words = query.split(/\s+/); + const wordMatchersForMysql = words.map( + (word) => "%" + word.replace(/_%/g, "\\$0") + "%" + ); + const matcherPlaceholders = words + .map((_) => "t.name LIKE ?") + .join(" AND "); const [rows, _] = await db.execute( `SELECT DISTINCT items.*, t.name FROM items INNER JOIN item_translations t ON t.item_id = items.id INNER JOIN parents_swf_assets rel ON rel.parent_type = "Item" AND rel.parent_id = items.id INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id - WHERE t.name LIKE ? AND t.locale="en" AND + WHERE ${matcherPlaceholders} AND t.locale="en" AND (swf_assets.body_id = ? OR swf_assets.body_id = 0) ORDER BY t.name LIMIT ? OFFSET ?`, - [queryForMysql, bodyId, actualLimit, actualOffset] + [...wordMatchersForMysql, bodyId, actualLimit, actualOffset] ); const entities = rows.map(normalizeRow); diff --git a/src/server/query-tests/ItemSearch.test.js b/src/server/query-tests/ItemSearch.test.js index 1e37a24..f54c43d 100644 --- a/src/server/query-tests/ItemSearch.test.js +++ b/src/server/query-tests/ItemSearch.test.js @@ -24,11 +24,58 @@ describe("ItemSearch", () => { Array [ "SELECT items.*, t.name FROM items INNER JOIN item_translations t ON t.item_id = items.id - WHERE t.name LIKE ? AND t.locale=\\"en\\" + WHERE t.name LIKE ? AND t.name LIKE ? AND t.locale=\\"en\\" ORDER BY t.name LIMIT 30", Array [ - "%Neopian Times%", + "%Neopian%", + "%Times%", + ], + ], + ] + `); + }); + + it("searches for each word separately", async () => { + const res = await query({ + query: gql` + query { + itemSearch(query: "Tarla Workshop") { + query + items { + id + name + } + } + } + `, + }); + + expect(res).toHaveNoErrors(); + expect(res.data).toMatchInlineSnapshot(` + Object { + "itemSearch": Object { + "items": Array [ + Object { + "id": "50377", + "name": "Tarlas Underground Workshop Background", + }, + ], + "query": "Tarla Workshop", + }, + } + `); + expect(getDbCalls()).toMatchInlineSnapshot(` + Array [ + Array [ + "SELECT items.*, t.name FROM items + INNER JOIN item_translations t ON t.item_id = items.id + WHERE t.name LIKE ? AND t.name LIKE ? AND t.locale=\\"en\\" + ORDER BY t.name + LIMIT 30", + Array [ + "%Tarla%", + "%Workshop%", ], ], ] @@ -71,12 +118,77 @@ describe("ItemSearch", () => { INNER JOIN parents_swf_assets rel ON rel.parent_type = \\"Item\\" AND rel.parent_id = items.id INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id - WHERE t.name LIKE ? AND t.locale=\\"en\\" AND + WHERE t.name LIKE ? AND t.name LIKE ? AND t.locale=\\"en\\" AND (swf_assets.body_id = ? OR swf_assets.body_id = 0) ORDER BY t.name LIMIT ? OFFSET ?", Array [ - "%Neopian Times%", + "%Neopian%", + "%Times%", + "180", + 30, + 0, + ], + ], + ] + `); + }); + + it("searches for each word separately (fit mode)", async () => { + const res = await query({ + query: gql` + query { + itemSearchToFit( + query: "Tarla Workshop" + speciesId: "54" + colorId: "75" + ) { + query + items { + id + name + } + } + } + `, + }); + + expect(res).toHaveNoErrors(); + expect(res.data).toMatchInlineSnapshot(` + Object { + "itemSearchToFit": Object { + "items": Array [ + Object { + "id": "50377", + "name": "Tarlas Underground Workshop Background", + }, + ], + "query": "Tarla Workshop", + }, + } + `); + expect(getDbCalls()).toMatchInlineSnapshot(` + Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT DISTINCT items.*, t.name FROM items + INNER JOIN item_translations t ON t.item_id = items.id + INNER JOIN parents_swf_assets rel + ON rel.parent_type = \\"Item\\" AND rel.parent_id = items.id + INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id + WHERE t.name LIKE ? AND t.name LIKE ? AND t.locale=\\"en\\" AND + (swf_assets.body_id = ? OR swf_assets.body_id = 0) + ORDER BY t.name + LIMIT ? OFFSET ?", + Array [ + "%Tarla%", + "%Workshop%", "180", 30, 0,