WIP: improve search #1

Closed
rob wants to merge 26 commits from improve-search into master
5 changed files with 131 additions and 25 deletions
Showing only changes of commit d08921573f - Show all commits

View File

@ -15,15 +15,55 @@ use actix_files::NamedFile;
use actix_web::{middleware, web, App, HttpResponse, HttpServer}; use actix_web::{middleware, web, App, HttpResponse, HttpServer};
use failure::Error; use failure::Error;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params; use rusqlite::{params, named_params, NO_PARAMS};
use std::env; use std::env;
use std::ops::Deref; use std::ops::Deref;
use std::cmp; use std::cmp;
use std::io; use std::io;
use std::fmt;
use serde_json::Value; use serde_json::Value;
const DEFAULT_SIZE: usize = 25; const DEFAULT_SIZE: usize = 25;
enum SortDir {
Asc,
Desc,
}
enum Sort {
Key(String),
Dir(SortDir),
}
#[derive(Copy, Clone, Debug, Deserialize)]
enum SortKey {
Name,
Size,
Seeds,
Leeches,
Scraped,
}
impl fmt::Display for SortKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Copy, Clone, Debug, Deserialize)]
enum SortDir {
Asc,
Desc,
}
impl fmt::Display for SortDir {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
println!("Access me at {}", endpoint()); println!("Access me at {}", endpoint());
@ -69,6 +109,8 @@ struct SearchQuery {
q: String, q: String,
page: Option<usize>, page: Option<usize>,
size: Option<usize>, size: Option<usize>,
sort_key: Option<SortKey>,
sort_dir: Option<SortDir>,
type_: Option<String>, type_: Option<String>,
} }
@ -94,6 +136,8 @@ async fn search(
struct NewQuery { struct NewQuery {
page: Option<usize>, page: Option<usize>,
size: Option<usize>, size: Option<usize>,
sort_key: Option<SortKey>,
sort_dir: Option<SortDir>,
type_: Option<String>, type_: Option<String>,
} }
@ -126,20 +170,22 @@ fn search_query(
} }
let page = query.page.unwrap_or(1); let page = query.page.unwrap_or(1);
let sort_key = query.sort_key.unwrap_or(SortKey::Name);
let sort_dir = query.sort_dir.unwrap_or(SortDir::Desc);
let size = cmp::min(100, query.size.unwrap_or(DEFAULT_SIZE)); let size = cmp::min(100, query.size.unwrap_or(DEFAULT_SIZE));
let type_ = query.type_.as_ref().map_or("torrent", String::deref); let type_ = query.type_.as_ref().map_or("torrent", String::deref);
let offset = size * (page - 1); let offset = size * (page - 1);
println!( dbg!(
"query = {}, type = {}, page = {}, size = {}", "query = {}, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}",
q, type_, page, size q, type_, page, size, sort_key, sort_dir
); );
let res = if type_ == "file" { let res = if type_ == "file" {
let results = torrent_file_search(conn, q, size, offset)?; let results = torrent_file_search(conn, q, size, offset)?;
serde_json::to_value(&results).unwrap() serde_json::to_value(&results).unwrap()
} else { } else {
let results = torrent_search(conn, q, size, offset)?; let results = torrent_search(conn, q, sort_key, sort_dir, size, offset)?;
serde_json::to_value(&results).unwrap() serde_json::to_value(&results).unwrap()
}; };
@ -153,12 +199,14 @@ fn new_query(
let page = query.page.unwrap_or(1); let page = query.page.unwrap_or(1);
let size = cmp::min(100, query.size.unwrap_or(DEFAULT_SIZE)); let size = cmp::min(100, query.size.unwrap_or(DEFAULT_SIZE));
let sort_key = query.sort_key.as_ref().unwrap_or(&SortKey::Name);
let sort_dir = query.sort_dir.as_ref().unwrap_or(&SortDir::Desc);
let type_ = query.type_.as_ref().map_or("torrent", String::deref); let type_ = query.type_.as_ref().map_or("torrent", String::deref);
let offset = size * (page - 1); let offset = size * (page - 1);
println!( dbg!(
"new, type = {}, page = {}, size = {}", "new, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}",
type_, page, size type_, page, size, sort_key, sort_dir
); );
let res = if type_ == "file" { let res = if type_ == "file" {
@ -187,18 +235,23 @@ struct Torrent {
fn torrent_search( fn torrent_search(
conn: r2d2::PooledConnection<SqliteConnectionManager>, conn: r2d2::PooledConnection<SqliteConnectionManager>,
query: &str, query: &str,
sort_key: SortKey,
sort_dir: SortDir,
size: usize, size: usize,
offset: usize, offset: usize,
) -> Result<Vec<Torrent>, Error> { ) -> Result<Vec<Torrent>, Error> {
let stmt_str = "select * from torrents where name like '%' || ?1 || '%' limit ?2, ?3"; let stmt_str = format!("select * from torrents where name like '%' || :query || '%' order by :sort_key {} limit :offset, :size", sort_dir.to_string().to_lowercase());
let mut stmt = conn.prepare(&stmt_str)?; let mut stmt = conn.prepare(&stmt_str)?;
let torrent_iter = stmt.query_map( let torrent_iter = stmt.query_map_named(
params![ named_params!{
query.replace(" ", "%"), ":query": query.replace(" ", "%"),
offset.to_string(), ":sort_key": sort_key.to_string().to_lowercase(),
size.to_string(), ":offset": offset.to_string(),
], ":size": size.to_string(),
},
|row| { |row| {
let size: isize = row.get(2)?;
println!("got size {:?}", size);
Ok(Torrent { Ok(Torrent {
infohash: row.get(0)?, infohash: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
@ -346,7 +399,7 @@ mod tests {
let manager = SqliteConnectionManager::file(super::torrents_db_file()); let manager = SqliteConnectionManager::file(super::torrents_db_file());
let pool = r2d2::Pool::builder().max_size(15).build(manager).unwrap(); let pool = r2d2::Pool::builder().max_size(15).build(manager).unwrap();
let conn = pool.get().unwrap(); let conn = pool.get().unwrap();
let results = super::torrent_search(conn, "sherlock", 10, 0); let results = super::torrent_search(conn, "sherlock", 10, 0, SortKey::Name, SortDir::Desc);
assert!(results.unwrap().len() > 2); assert!(results.unwrap().len() > 2);
// println!("Query took {:?} seconds.", end - start); // println!("Query took {:?} seconds.", end - start);
} }

View File

@ -13,6 +13,8 @@ export class Navbar extends Component<any, State> {
searchParams: { searchParams: {
page: 1, page: 1,
q: "", q: "",
sort_key: 'Name',
sort_dir: 'Desc',
type_: "torrent" type_: "torrent"
} }
} }
@ -70,13 +72,15 @@ export class Navbar extends Component<any, State> {
search(i: Navbar, event) { search(i: Navbar, event) {
event.preventDefault(); event.preventDefault();
i.context.router.history.push(`/search/${i.state.searchParams.type_}/${i.state.searchParams.q}/${i.state.searchParams.page}`); i.context.router.history.push(`/search/${i.state.searchParams.type_}/${i.state.searchParams.q}/${i.state.searchParams.page}/${i.state.searchParams.sort_key}/${i.state.searchParams.sort_dir}`);
} }
searchChange(i: Navbar, event) { searchChange(i: Navbar, event) {
let searchParams: SearchParams = { let searchParams: SearchParams = {
q: event.target.value, q: event.target.value,
page: 1, page: 1,
sort_key: 'Name',
sort_dir: 'Desc',
type_: i.state.searchParams.type_ type_: i.state.searchParams.type_
} }
i.setState({ searchParams: searchParams }); i.setState({ searchParams: searchParams });
@ -86,6 +90,8 @@ export class Navbar extends Component<any, State> {
let searchParams: SearchParams = { let searchParams: SearchParams = {
q: i.state.searchParams.q, q: i.state.searchParams.q,
page: 1, page: 1,
sort_key: 'Name',
sort_dir: 'Desc',
type_: event.target.value type_: event.target.value
} }
i.setState({ searchParams: searchParams }); i.setState({ searchParams: searchParams });
@ -96,6 +102,8 @@ export class Navbar extends Component<any, State> {
this.state.searchParams = { this.state.searchParams = {
page: Number(splitPath[4]), page: Number(splitPath[4]),
q: splitPath[3], q: splitPath[3],
sort_key: splitPath[5],
sort_dir: splitPath[6],
type_: splitPath[2] type_: splitPath[2]
}; };
} }

View File

@ -11,6 +11,25 @@ interface State {
searching: Boolean; searching: Boolean;
} }
function buildSearchURL(sort_key, {state: { searchParams }) {
console.log("got search URL sort_key", sort_key, "searchParams", searchParams, "thing", searchParams);
//let searchParams = thing.state.searchParams;
if (sort_key === searchParams.sort_key) {
const sort_dir = searchParams.sort_dir === "Asc" ? "Desc" : "Asc";
searchParams.sort_dir = sort_dir;
console.log("no change in sort key from", sort_key, "so switch direction instead.")
} else {
console.log("change sort key from", searchParams.sort_key, "to", sort_key);
searchParams.sort_key = sort_key;
}
const url = `/#/search/${searchParams.type_}/${searchParams.q}/${searchParams.page}/${searchParams.sort_key}/${searchParams.sort_dir}`
return url;
}
export class Search extends Component<any, State> { export class Search extends Component<any, State> {
state: State = { state: State = {
@ -20,6 +39,8 @@ export class Search extends Component<any, State> {
searchParams: { searchParams: {
q: "", q: "",
page: 1, page: 1,
sort_key: 'Name',
sort_dir: 'Desc',
type_: 'torrent' type_: 'torrent'
}, },
searching: false searching: false
@ -30,9 +51,12 @@ export class Search extends Component<any, State> {
} }
componentDidMount() { componentDidMount() {
console.log("got props", this.props);
this.state.searchParams = { this.state.searchParams = {
page: Number(this.props.match.params.page), page: Number(this.props.match.params.page),
q: this.props.match.params.q, q: this.props.match.params.q,
sort_key: this.props.match.params.sort_key,
sort_dir: this.props.match.params.sort_dir,
type_: this.props.match.params.type_ type_: this.props.match.params.type_
} }
this.search(); this.search();
@ -44,6 +68,8 @@ export class Search extends Component<any, State> {
this.state.searchParams = { this.state.searchParams = {
page: Number(this.props.match.params.page), page: Number(this.props.match.params.page),
q: this.props.match.params.q, q: this.props.match.params.q,
sort_key: this.props.match.params.sort_key,
sort_dir: this.props.match.params.sort_dir,
type_: this.props.match.params.type_ type_: this.props.match.params.type_
} }
this.search(); this.search();
@ -52,6 +78,7 @@ export class Search extends Component<any, State> {
} }
search() { search() {
console.log("in search, state", this.state);
if (!!this.state.searchParams.q) { if (!!this.state.searchParams.q) {
this.setState({ searching: true, results: { torrents: [] } }); this.setState({ searching: true, results: { torrents: [] } });
this.fetchData(this.state.searchParams) this.fetchData(this.state.searchParams)
@ -73,7 +100,7 @@ export class Search extends Component<any, State> {
fetchData(searchParams: SearchParams): Promise<Array<Torrent>> { fetchData(searchParams: SearchParams): Promise<Array<Torrent>> {
let q = encodeURI(searchParams.q); let q = encodeURI(searchParams.q);
return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}&type_=${searchParams.type_}`) return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}&sort_key=${searchParams.sort_key}&sort_dir=${searchParams.sort_dir}&type_=${searchParams.type_}`)
.then(data => data.json()); .then(data => data.json());
} }
@ -112,11 +139,27 @@ export class Search extends Component<any, State> {
<table class="table table-fixed table-hover table-sm table-striped table-hover-purple table-padding"> <table class="table table-fixed table-hover table-sm table-striped table-hover-purple table-padding">
<thead> <thead>
<tr> <tr>
<th class="search-name-col">Name</th> <th class="search-name-col">
<th class="text-right">Size</th> <a
<th class="text-right">Seeds</th> href={buildSearchURL('Name', this)}>
<th class="text-right d-none d-md-table-cell">Leeches</th> Name
<th class="text-right d-none d-md-table-cell">Scraped</th> </a>
</th>
<th class="text-right">
<a
href={buildSearchURL('Size', this)}>
Size
</a>
</th>
<th class="text-right">
<a href="#">Seeds</a>
</th>
<th class="text-right d-none d-md-table-cell">
<a href="#">Leeches</a>
</th>
<th class="text-right d-none d-md-table-cell">
<a href="#">Scraped</a>
</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -207,7 +250,7 @@ export class Search extends Component<any, State> {
switchPage(a: { i: Search, nextPage: boolean }) { switchPage(a: { i: Search, nextPage: boolean }) {
let newSearch = a.i.state.searchParams; let newSearch = a.i.state.searchParams;
newSearch.page += (a.nextPage) ? 1 : -1; newSearch.page += (a.nextPage) ? 1 : -1;
a.i.props.history.push(`/search/${newSearch.type_}/${newSearch.q}/${newSearch.page}`); a.i.props.history.push(`/search/${newSearch.type_}/${newSearch.q}/${newSearch.page}/${newSearch.sort_key}/${newSearch.sort_dir}`);
} }
copyLink(evt) { copyLink(evt) {

View File

@ -17,7 +17,7 @@ class Index extends Component<any, any> {
<div class="mt-3 p-0"> <div class="mt-3 p-0">
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route path={`/search/:type_/:q/:page`} component={Search} /> <Route path={`/search/:type_/:q/:page/:sort_key/:sort_dir`} component={Search} />
</Switch> </Switch>
{this.symbols()} {this.symbols()}
</div> </div>

View File

@ -3,6 +3,8 @@ export interface SearchParams {
page: number; page: number;
size?: number; size?: number;
type_: string; type_: string;
sort_key: string;
sort_dir: string;
} }
export interface Results { export interface Results {