Skip to content
Open
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
1 change: 1 addition & 0 deletions src/db/migrations/010-add-foreign-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function up(db) {
);
CREATE INDEX idx_foreign_card_name ON card_foreign_data(card_name);
CREATE INDEX idx_foreign_language ON card_foreign_data(language);
CREATE INDEX idx_foreign_name ON card_foreign_data(foreign_name);
`);
console.log('✓ Created card_foreign_data table');
}
Expand Down
16 changes: 16 additions & 0 deletions src/db/migrations/017-add-card-search-fts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export function up(db) {
// FTS5 table for fast multilingual searching across card names and foreign names
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS card_search USING fts5(
name,
foreign_names,
card_id UNINDEXED
);
`);
console.log('Created card_search FTS5 table');
}

export function down(db) {
db.exec(`DROP TABLE IF EXISTS card_search;`);
console.log('Dropped card_search FTS5 table');
}
46 changes: 28 additions & 18 deletions src/services/cardService.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,45 @@ 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) {
const searchTerm = `%${query}%`;
const prefix = `${query}%`;
const infix = `%${query}%`;

// Update the query to include image_url
const cards = db.all(
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 ? LIMIT 1) 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 ? LIMIT 1) THEN 3
ELSE 4
END as match_priority
FROM cards c
LEFT JOIN printings p ON p.card_id = c.id
LEFT JOIN sets s ON p.set_code = s.code
WHERE c.name LIKE ?
ORDER BY
CASE
WHEN c.name LIKE ? THEN 0
ELSE 1
END,
c.name
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 ? LIMIT 1)
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 ? LIMIT 1)
ORDER BY match_priority, c.name, p.set_code, p.collector_number
LIMIT ?`,
[searchTerm, `${query}%`, limit]
[prefix, infix, prefix, infix, prefix, infix, prefix, infix, limit]
);

// 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