Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions scripts/benchmark-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { performance } from 'perf_hooks';
import { searchCards } from '../src/services/cardService.js';

async function run() {
const queries = ['lightning', 'island', 'sol', 'dragon', 'guild', 'counterspell'];
console.log('Running search benchmarks (warmup + timed runs)...');

// Warmup
for (const q of queries) {
searchCards(q, 10);
}

const results = [];
for (const q of queries) {
const runs = 5;
let total = 0;
for (let i = 0; i < runs; i++) {
const t0 = performance.now();
const res = searchCards(q, 20);
const t1 = performance.now();
const ms = t1 - t0;
total += ms;
}
const avg = total / runs;
results.push({ query: q, avg_ms: avg });
}

console.log('Benchmark results:');
for (const r of results) {
console.log(`- ${r.query}: ${r.avg_ms.toFixed(2)} ms (avg)`);
}
}

run().catch(err => {
console.error('Benchmark failed:', err);
process.exit(1);
});
8 changes: 4 additions & 4 deletions scripts/import-mtgjson.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ async function importCards(sourceDb, targetDb) {
// Import foreign data
console.log('Importing foreign card data...');
const foreignData = srcDb.prepare(`
SELECT c.name, fd.language, fd.faceName, fd.text, fd.type, fd.flavorText
SELECT c.name as english_name, fd.language, fd.name as faceName, fd.text, fd.type, fd.flavorText
FROM cardForeignData fd
JOIN cards c ON fd.uuid = c.uuid
WHERE c.name IS NOT NULL AND fd.language IS NOT NULL
Expand All @@ -460,12 +460,12 @@ async function importCards(sourceDb, targetDb) {
let inserted = 0;
for (const fd of data) {
// Only insert if the card exists in our target database
const cardExists = targetDb.prepare('SELECT 1 FROM cards WHERE name = ? LIMIT 1').get(fd.name);
const cardExists = targetDb.prepare('SELECT 1 FROM cards WHERE name = ? LIMIT 1').get(fd.english_name);
if (cardExists) {
insertForeign.run(
fd.name,
fd.english_name, // card_name: English card name
fd.language,
fd.faceName,
fd.faceName, // foreign_name: translated name
fd.text,
fd.type,
fd.flavorText
Expand Down
14 changes: 14 additions & 0 deletions src/db/migrations/020-add-foreign-name-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function up(db) {
// Index foreign_name so multilingual card search can use it.
// This lives in its own migration (rather than being added to 010) because 010 has
// already been applied on existing installs and would never re-run for them.
db.exec(`
CREATE INDEX IF NOT EXISTS idx_foreign_name ON card_foreign_data(foreign_name);
`);
console.log('✓ Added idx_foreign_name index on card_foreign_data');
}

export function down(db) {
db.exec(`DROP INDEX IF EXISTS idx_foreign_name;`);
console.log('✓ Dropped idx_foreign_name index');
}
60 changes: 38 additions & 22 deletions src/services/cardService.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,56 @@ function generateImageUrls(uuid) {
}

/**
* Search cards by name (for autocomplete)
* Search cards by name (autocomplete) - returns individual printings
* Supports English and foreign names, plus second parts of split cards.
*/
export function searchCards(query, limit = 20, typeFilter = null) {
const searchTerm = `%${query}%`;
const prefix = `${query}%`;
const infix = `%${query}%`;
// Optional card-type filter (used by the price-watch type filter) must apply to the
// whole name-match group, hence the parenthesised OR block below.
const typeClause = typeFilter ? 'AND c.type_line LIKE ?' : '';
// Param order must match SQL placeholder order:
// 1. WHERE c.name LIKE ? 2. [AND type_line LIKE ?] 3. WHEN c.name LIKE ? 4. LIMIT ?
const params = typeFilter
? [searchTerm, `%${typeFilter}%`, `${query}%`, limit]
: [searchTerm, `${query}%`, limit];

const cards = db.all(
// Placeholder order: 4 for match_priority CASE, 4 for the WHERE name group,
// then [type filter], then LIMIT.
const params = [prefix, infix, prefix, infix, prefix, infix, prefix, infix];
if (typeFilter) params.push(`%${typeFilter}%`);
params.push(limit);

const rows = db.all(
`SELECT c.id, c.name, c.mana_cost, c.cmc, c.colors, c.type_line, c.oracle_text,
p.image_url,
(SELECT p.uuid FROM printings p WHERE p.card_id = c.id LIMIT 1) as sample_uuid
p.set_code,
s.name as set_name,
p.collector_number,
p.rarity,
p.uuid as sample_uuid,
CASE
WHEN c.name LIKE ? THEN 0
WHEN c.name LIKE '%//%' AND c.name LIKE ? THEN 1
WHEN EXISTS(SELECT 1 FROM card_foreign_data f WHERE f.card_name = c.name AND f.foreign_name LIKE ?) THEN 2
WHEN EXISTS(SELECT 1 FROM card_foreign_data f WHERE f.card_name = c.name AND f.foreign_name LIKE '%//%' AND f.foreign_name LIKE ?) THEN 3
ELSE 4
END as match_priority
FROM cards c
LEFT JOIN printings p ON p.card_id = c.id
WHERE c.name LIKE ? ${typeClause}
ORDER BY
CASE
WHEN c.name LIKE ? THEN 0
ELSE 1
END,
c.name
LEFT JOIN sets s ON p.set_code = s.code
WHERE (
c.name LIKE ?
OR (c.name LIKE '%//%' AND c.name LIKE ?)
OR EXISTS(SELECT 1 FROM card_foreign_data f WHERE f.card_name = c.name AND f.foreign_name LIKE ?)
OR EXISTS(SELECT 1 FROM card_foreign_data f WHERE f.card_name = c.name AND f.foreign_name LIKE '%//%' AND f.foreign_name LIKE ?)
) ${typeClause}
ORDER BY match_priority, c.name, p.set_code, p.collector_number
LIMIT ?`,
params
);

// Add image URLs from database
return cards.map(card => ({
...card,
image_url: card.image_url,
large_image_url: card.image_url ? card.image_url.replace('/normal/', '/large/') : null,
art_crop_url: card.image_url ? card.image_url.replace('/normal/', '/art_crop/') : null
return rows.map(row => ({
...row,
image_url: row.image_url,
large_image_url: row.image_url ? row.image_url.replace('/normal/', '/large/') : null,
art_crop_url: row.image_url ? row.image_url.replace('/normal/', '/art_crop/') : null
}));
}

Expand Down
Loading