Moved string matching to sqlite

Probably (definitely more efficient)
This commit is contained in:
2025-08-20 21:07:37 +01:00
parent 6ff0204189
commit 3f7cd6353a
4 changed files with 51 additions and 23 deletions

View File

@@ -9,6 +9,7 @@ edition = "2021"
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.42", features = ["derive"] } clap = { version = "4.5.42", features = ["derive"] }
closestmatch = "0.1.2" closestmatch = "0.1.2"
deunicode = "1.6.2"
dir_spec = "0.2.0" dir_spec = "0.2.0"
rusqlite = "0.37.0" rusqlite = "0.37.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,8 +1,8 @@
use deunicode::deunicode;
use rusqlite; use rusqlite;
use std::fmt; use std::fmt;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::SplitWhitespace;
use super::deser::ScryfallCard; use super::deser::ScryfallCard;
use super::utils::get_local_cache_folder; use super::utils::get_local_cache_folder;
@@ -46,13 +46,14 @@ pub fn update_db_with_file(file: PathBuf) -> bool {
let tx = conn.transaction().unwrap(); let tx = conn.transaction().unwrap();
for card in ac { for card in ac {
for word in card.name.split_whitespace() { for word in card.name.split_whitespace() {
let word = deunicode(word);
let res = tx.execute( let res = tx.execute(
"INSERT INTO magic_words (word) VALUES (?1) "INSERT INTO magic_words (word) VALUES (?1)
ON CONFLICT (word) DO NOTHING;", ON CONFLICT (word) DO NOTHING;",
[word.replace(",", "")], [word.replace(",", "")],
); );
} }
let lowercase_name = card.name.to_lowercase(); let lowercase_name = deunicode(&card.name.to_lowercase());
let power_toughness = match card.power { let power_toughness = match card.power {
Some(p) => Some(format!("{}/{}", p, card.toughness.unwrap())), Some(p) => Some(format!("{}/{}", p, card.toughness.unwrap())),
None => None, None => None,
@@ -107,6 +108,7 @@ impl fmt::Display for DbCard {
#[derive(Debug)] #[derive(Debug)]
pub struct DbCard { pub struct DbCard {
pub name: String, pub name: String,
pub lowercase_name: String,
pub type_line: String, pub type_line: String,
pub oracle_text: Option<String>, pub oracle_text: Option<String>,
pub power_toughness: Option<String>, pub power_toughness: Option<String>,
@@ -125,11 +127,11 @@ pub fn get_card_by_name(name: &str, name_type: GetNameType) -> Option<DbCard> {
let conn = rusqlite::Connection::open(sqlite_file).unwrap(); let conn = rusqlite::Connection::open(sqlite_file).unwrap();
let sql = match name_type { let sql = match name_type {
GetNameType::Name => { GetNameType::Name => {
"SELECT name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
FROM cards WHERE name = (?1)" FROM cards WHERE name = (?1)"
} }
GetNameType::LowercaseName => { GetNameType::LowercaseName => {
"SELECT name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri "SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
FROM cards WHERE lowercase_name = (?1)" FROM cards WHERE lowercase_name = (?1)"
} }
}; };
@@ -138,17 +140,48 @@ pub fn get_card_by_name(name: &str, name_type: GetNameType) -> Option<DbCard> {
match rows.next().unwrap() { match rows.next().unwrap() {
Some(row) => Some(DbCard { Some(row) => Some(DbCard {
name: row.get(0).unwrap(), name: row.get(0).unwrap(),
type_line: row.get(1).unwrap(), lowercase_name: row.get(1).unwrap(),
oracle_text: row.get(2).unwrap(), type_line: row.get(2).unwrap(),
power_toughness: row.get(3).unwrap(), oracle_text: row.get(3).unwrap(),
loyalty: row.get(4).unwrap(), power_toughness: row.get(4).unwrap(),
mana_cost: row.get(5).unwrap(), loyalty: row.get(5).unwrap(),
scryfall_uri: row.get(6).unwrap(), mana_cost: row.get(6).unwrap(),
scryfall_uri: row.get(7).unwrap(),
}), }),
None => None, None => None,
} }
} }
pub fn find_matching_cards(name: &str) -> Vec<DbCard> {
let sqlite_file = get_local_data_sqlite_file();
let conn = rusqlite::Connection::open(sqlite_file).unwrap();
// There must be something better than this - although I don't think it's possible with a str
let mut name = name.to_string();
name.push('%');
name.insert(0, '%');
let mut stmt = conn
.prepare(
"SELECT name, lowercase_name, type_line, oracle_text, power_toughness, loyalty, mana_cost, scryfall_uri
FROM cards WHERE lowercase_name LIKE (?1)",
)
.unwrap();
stmt.query_map([name], |row| {
Ok(DbCard {
name: row.get(0).unwrap(),
lowercase_name: row.get(1).unwrap(),
type_line: row.get(2).unwrap(),
oracle_text: row.get(3).unwrap(),
power_toughness: row.get(4).unwrap(),
loyalty: row.get(5).unwrap(),
mana_cost: row.get(6).unwrap(),
scryfall_uri: row.get(7).unwrap(),
})
})
.unwrap()
.filter_map(|res| res.ok())
.collect()
}
const CREATE_CARDS_TABLE_SQL: &str = " const CREATE_CARDS_TABLE_SQL: &str = "
CREATE TABLE cards ( CREATE TABLE cards (
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,

View File

@@ -6,8 +6,8 @@ pub use crate::deser::ScryfallCard;
mod db; mod db;
pub use db::{ pub use db::{
get_all_card_names, get_all_lowercase_card_names, get_card_by_name, init_db, find_matching_cards, get_all_card_names, get_all_lowercase_card_names, get_card_by_name,
update_db_with_file, GetNameType, init_db, update_db_with_file, GetNameType,
}; };
mod utils; mod utils;

View File

@@ -1,4 +1,5 @@
use clap::Parser; use clap::Parser;
use scryfall_deser::find_matching_cards;
use scryfall_deser::get_all_lowercase_card_names; use scryfall_deser::get_all_lowercase_card_names;
use scryfall_deser::get_card_by_name; use scryfall_deser::get_card_by_name;
use scryfall_deser::get_local_cache_folder; use scryfall_deser::get_local_cache_folder;
@@ -55,26 +56,19 @@ fn main() {
} }
search_string.pop(); search_string.pop();
// This section should be replaced with a SQL command let matching_cards = find_matching_cards(&search_string);
let cards = get_all_lowercase_card_names();
let mut matching_cards = Vec::new();
for card in cards {
if card.contains(&search_string) {
matching_cards.push(card.clone());
}
}
dbg!(&matching_cards); dbg!(&matching_cards);
if matching_cards.is_empty() { if matching_cards.is_empty() {
// Do some distance checking stuff // Do some distance checking stuff
} else if matching_cards.len() == 1 { } else if matching_cards.len() == 1 {
let card = get_card_by_name(&matching_cards[0], GetNameType::LowercaseName); let card = get_card_by_name(&matching_cards[0].name, GetNameType::LowercaseName);
dbg!(card); dbg!(card);
} else { } else {
for card in matching_cards { for card in matching_cards {
println!( println!(
"{}", "{}",
get_card_by_name(&card, GetNameType::LowercaseName) get_card_by_name(&card.lowercase_name, GetNameType::LowercaseName)
.unwrap() .unwrap()
.name .name
); );