Added basic query functionality to frontend.
This commit is contained in:
parent
c39dd9cc03
commit
0d941e82d2
8 changed files with 224 additions and 14 deletions
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
|
|
@ -7,15 +7,15 @@
|
|||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'quiksearch'",
|
||||
"name": "Debug executable 'quicksearch'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=quiksearch",
|
||||
"--package=quiksearch"
|
||||
"--bin=quicksearch",
|
||||
"--package=quicksearch"
|
||||
],
|
||||
"filter": {
|
||||
"name": "quiksearch",
|
||||
"name": "quicksearch",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
|
|
@ -25,16 +25,16 @@
|
|||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'quiksearch'",
|
||||
"name": "Debug unit tests in executable 'quicksearch'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=quiksearch",
|
||||
"--package=quiksearch"
|
||||
"--bin=quicksearch",
|
||||
"--package=quicksearch"
|
||||
],
|
||||
"filter": {
|
||||
"name": "quiksearch",
|
||||
"name": "quicksearch",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3103,7 +3103,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "quiksearch"
|
||||
name = "quicksearch"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "quiksearch"
|
||||
name = "quicksearch"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ default_indexing_path = "C:\\"
|
|||
database_path = "QuickSearch.db"
|
||||
|
||||
[processing]
|
||||
# Size of hash blocks read from start/end of files (bytes)
|
||||
hash_length = 4096
|
||||
# Amount of data in bytes read from start/end of files used to calculate hash
|
||||
hash_length = 8192
|
||||
# Maximum text content to index per file (bytes)
|
||||
maximum_text_size = 524288
|
||||
# Maximum file size to process for text extraction (bytes)
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ pub fn App(props: AppProps) -> Element {
|
|||
let mut config_changes = use_signal(|| Vec::<String>::new());
|
||||
let speed_tracker = use_signal(|| SpeedTracker::new());
|
||||
|
||||
|
||||
let indexing_service_for_start = props.indexing_service.clone();
|
||||
let indexing_service_for_start_dialog = props.indexing_service.clone();
|
||||
let indexing_service_for_stop = props.indexing_service.clone();
|
||||
|
|
@ -281,6 +282,11 @@ pub fn App(props: AppProps) -> Element {
|
|||
"{status_text}"
|
||||
}
|
||||
}
|
||||
|
||||
crate::search::Search {
|
||||
indexing_service: props.indexing_service.clone(),
|
||||
db_path: db_path().clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration validation dialog
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ use rusqlite::{Connection, params};
|
|||
use crate::file_handling::{load_existing_files, analyze_files_for_batch_update, process_batch_updates_files_only, process_batch_inserts_files_only, process_text_indexing};
|
||||
use crate::config::Config;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SearchResultRow {
|
||||
pub values: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SearchResult {
|
||||
pub columns: Vec<String>,
|
||||
pub rows: Vec<SearchResultRow>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IndexingStatus {
|
||||
Idle,
|
||||
|
|
@ -149,6 +160,49 @@ impl IndexingService {
|
|||
self.stop_indexing()
|
||||
}
|
||||
|
||||
/// Execute a search query against the database
|
||||
pub fn execute_search(&self, db_path: &str, query: &str) -> Result<Vec<SearchResult>, String> {
|
||||
let conn = Connection::open(db_path)
|
||||
.map_err(|e| format!("Failed to open database: {}", e))?;
|
||||
|
||||
let mut stmt = conn.prepare(query)
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
let column_count = stmt.column_count();
|
||||
let column_names: Vec<String> = (0..column_count)
|
||||
.map(|i| stmt.column_name(i).unwrap_or("").to_string())
|
||||
.collect();
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let mut values = Vec::new();
|
||||
for i in 0..column_count {
|
||||
let value = match row.get_ref(i)? {
|
||||
rusqlite::types::ValueRef::Null => "NULL".to_string(),
|
||||
rusqlite::types::ValueRef::Integer(i) => i.to_string(),
|
||||
rusqlite::types::ValueRef::Real(f) => f.to_string(),
|
||||
rusqlite::types::ValueRef::Text(t) => String::from_utf8_lossy(t).to_string(),
|
||||
rusqlite::types::ValueRef::Blob(b) => format!("BLOB({} bytes)", b.len()),
|
||||
};
|
||||
values.push(value);
|
||||
}
|
||||
Ok(SearchResultRow { values })
|
||||
})
|
||||
.map_err(|e| format!("Failed to execute query: {}", e))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for row in rows {
|
||||
match row {
|
||||
Ok(search_row) => results.push(search_row),
|
||||
Err(e) => return Err(format!("Error reading row: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![SearchResult {
|
||||
columns: column_names,
|
||||
rows: results,
|
||||
}])
|
||||
}
|
||||
|
||||
/// Check if configuration changes require index recreation
|
||||
pub fn check_config_validation(&self, db_path: &str, config: &Config, indexing_path: &str) -> Result<Option<Vec<String>>, String> {
|
||||
let conn = Connection::open(db_path)
|
||||
|
|
|
|||
10
src/main.rs
10
src/main.rs
|
|
@ -6,6 +6,7 @@ mod file_handling;
|
|||
mod document_extraction;
|
||||
mod indexing;
|
||||
mod config;
|
||||
mod search;
|
||||
|
||||
// Global indexing service for signal handling
|
||||
static INDEXING_SERVICE: OnceLock<Arc<indexing::IndexingService>> = OnceLock::new();
|
||||
|
|
@ -49,7 +50,12 @@ fn app() -> Element {
|
|||
}
|
||||
|
||||
/*
|
||||
SELECT name, hash, count(*) as cnt FROM files GROUP BY hash ORDER BY cnt DESC;
|
||||
Duplicate files:
|
||||
SELECT name, count(*) as cnt, path FROM files GROUP BY hash HAVING cnt > 1 ORDER BY cnt DESC;
|
||||
|
||||
SELECT name, path, text, snippet(searchabletext, 2 , "<b>", "</b>", "...", 64) as "snip" FROM searchabletext WHERE text MATCH 'Terrasound' LIMIT 100;
|
||||
Full text search:
|
||||
SELECT name, path, text, snippet(searchabletext, 2 , "<b>", "</b>", "<b>...</b>", 64) as "snip" FROM searchabletext WHERE text MATCH 'searchstring'
|
||||
|
||||
Filename search:
|
||||
SELECT name, path FROM files WHERE name LIKE '%searchstring%';
|
||||
*/
|
||||
144
src/search.rs
Normal file
144
src/search.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
use std::sync::Arc;
|
||||
use dioxus::prelude::*;
|
||||
use crate::indexing::{IndexingService, SearchResult};
|
||||
|
||||
#[derive(Props, Clone)]
|
||||
pub struct SearchProps {
|
||||
pub indexing_service: Arc<IndexingService>,
|
||||
pub db_path: String,
|
||||
}
|
||||
|
||||
impl PartialEq for SearchProps {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.indexing_service, &other.indexing_service) && self.db_path == other.db_path
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Search(props: SearchProps) -> Element {
|
||||
let mut search_type = use_signal(|| "fulltext".to_string());
|
||||
let mut search_term = use_signal(|| String::new());
|
||||
let mut search_results = use_signal(|| Vec::<SearchResult>::new());
|
||||
let mut search_error = use_signal(|| None::<String>);
|
||||
|
||||
let service = props.indexing_service.clone();
|
||||
let db_path = props.db_path.clone();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
style: "margin-top: 30px;",
|
||||
h2 { "Search Database" }
|
||||
|
||||
div {
|
||||
style: "margin-bottom: 10px;",
|
||||
label { "Search Type: " }
|
||||
select {
|
||||
value: "{search_type}",
|
||||
onchange: move |evt| search_type.set(evt.value()),
|
||||
option { value: "fulltext", "Full Text Search" }
|
||||
option { value: "filename", "Filename Search" }
|
||||
option { value: "duplicates", "Find Duplicate Files" }
|
||||
}
|
||||
}
|
||||
|
||||
if search_type() != "duplicates" {
|
||||
div {
|
||||
style: "margin-bottom: 10px;",
|
||||
label { "Search Term: " }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{search_term}",
|
||||
oninput: move |evt| search_term.set(evt.value())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
style: "padding: 10px 20px; background-color: #2196F3; color: white; border: none; cursor: pointer;",
|
||||
onclick: move |_| {
|
||||
let service_clone = service.clone();
|
||||
let db_clone = db_path.clone();
|
||||
let search_type_val = search_type().clone();
|
||||
let search_term_val = search_term().clone();
|
||||
|
||||
let mut search_results_clone = search_results.clone();
|
||||
let mut search_error_clone = search_error.clone();
|
||||
|
||||
spawn(async move {
|
||||
search_error_clone.set(None);
|
||||
|
||||
let query = match search_type_val.as_str() {
|
||||
"fulltext" => {
|
||||
if search_term_val.trim().is_empty() {
|
||||
search_error_clone.set(Some("Please enter a search term".to_string()));
|
||||
return;
|
||||
}
|
||||
format!("SELECT name, path, snippet(searchabletext, 2, '<b>', '</b>', '<b>...</b>', 64) as snippet FROM searchabletext WHERE text MATCH '{}'", search_term_val.replace("'", "''"))
|
||||
},
|
||||
"filename" => {
|
||||
if search_term_val.trim().is_empty() {
|
||||
search_error_clone.set(Some("Please enter a filename pattern".to_string()));
|
||||
return;
|
||||
}
|
||||
format!("SELECT name, path FROM files WHERE name LIKE '%{}%'", search_term_val.replace("'", "''"))
|
||||
},
|
||||
"duplicates" => "SELECT name, count(*) as cnt, path FROM files GROUP BY hash HAVING cnt > 1 ORDER BY cnt DESC".to_string(),
|
||||
_ => return
|
||||
};
|
||||
|
||||
match service_clone.execute_search(&db_clone, &query) {
|
||||
Ok(results) => search_results_clone.set(results),
|
||||
Err(e) => search_error_clone.set(Some(e))
|
||||
}
|
||||
});
|
||||
},
|
||||
"Search"
|
||||
}
|
||||
|
||||
if let Some(error) = search_error() {
|
||||
div {
|
||||
style: "color: red; margin-top: 10px;",
|
||||
"Error: {error}"
|
||||
}
|
||||
}
|
||||
|
||||
if !search_results().is_empty() {
|
||||
div {
|
||||
style: "margin-top: 20px;",
|
||||
h3 { "Search Results ({search_results()[0].rows.len()} rows)" }
|
||||
div {
|
||||
style: "max-height: 400px; overflow: auto; border: 1px solid #ddd;",
|
||||
table {
|
||||
style: "width: 100%; border-collapse: collapse; font-size: 12px;",
|
||||
thead {
|
||||
style: "background-color: #f5f5f5; position: sticky; top: 0;",
|
||||
tr {
|
||||
for column in search_results()[0].columns.iter() {
|
||||
th {
|
||||
style: "padding: 8px; border: 1px solid #ddd; text-align: left;",
|
||||
"{column}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for (i, row) in search_results()[0].rows.iter().enumerate() {
|
||||
tr {
|
||||
style: if i % 2 == 0 { "background-color: #f9f9f9;" } else { "" },
|
||||
for value in row.values.iter() {
|
||||
td {
|
||||
style: "padding: 8px; border: 1px solid #ddd; word-break: break-all;",
|
||||
dangerous_inner_html: "{value}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue