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",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug executable 'quiksearch'",
|
"name": "Debug executable 'quicksearch'",
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"--bin=quiksearch",
|
"--bin=quicksearch",
|
||||||
"--package=quiksearch"
|
"--package=quicksearch"
|
||||||
],
|
],
|
||||||
"filter": {
|
"filter": {
|
||||||
"name": "quiksearch",
|
"name": "quicksearch",
|
||||||
"kind": "bin"
|
"kind": "bin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -25,16 +25,16 @@
|
||||||
{
|
{
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug unit tests in executable 'quiksearch'",
|
"name": "Debug unit tests in executable 'quicksearch'",
|
||||||
"cargo": {
|
"cargo": {
|
||||||
"args": [
|
"args": [
|
||||||
"test",
|
"test",
|
||||||
"--no-run",
|
"--no-run",
|
||||||
"--bin=quiksearch",
|
"--bin=quicksearch",
|
||||||
"--package=quiksearch"
|
"--package=quicksearch"
|
||||||
],
|
],
|
||||||
"filter": {
|
"filter": {
|
||||||
"name": "quiksearch",
|
"name": "quicksearch",
|
||||||
"kind": "bin"
|
"kind": "bin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3103,7 +3103,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quiksearch"
|
name = "quicksearch"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "quiksearch"
|
name = "quicksearch"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ default_indexing_path = "C:\\"
|
||||||
database_path = "QuickSearch.db"
|
database_path = "QuickSearch.db"
|
||||||
|
|
||||||
[processing]
|
[processing]
|
||||||
# Size of hash blocks read from start/end of files (bytes)
|
# Amount of data in bytes read from start/end of files used to calculate hash
|
||||||
hash_length = 4096
|
hash_length = 8192
|
||||||
# Maximum text content to index per file (bytes)
|
# Maximum text content to index per file (bytes)
|
||||||
maximum_text_size = 524288
|
maximum_text_size = 524288
|
||||||
# Maximum file size to process for text extraction (bytes)
|
# Maximum file size to process for text extraction (bytes)
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ pub fn App(props: AppProps) -> Element {
|
||||||
let mut show_config_dialog = use_signal(|| false);
|
let mut show_config_dialog = use_signal(|| false);
|
||||||
let mut config_changes = use_signal(|| Vec::<String>::new());
|
let mut config_changes = use_signal(|| Vec::<String>::new());
|
||||||
let speed_tracker = use_signal(|| SpeedTracker::new());
|
let speed_tracker = use_signal(|| SpeedTracker::new());
|
||||||
|
|
||||||
|
|
||||||
let indexing_service_for_start = props.indexing_service.clone();
|
let indexing_service_for_start = props.indexing_service.clone();
|
||||||
let indexing_service_for_start_dialog = props.indexing_service.clone();
|
let indexing_service_for_start_dialog = props.indexing_service.clone();
|
||||||
|
|
@ -281,6 +282,11 @@ pub fn App(props: AppProps) -> Element {
|
||||||
"{status_text}"
|
"{status_text}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crate::search::Search {
|
||||||
|
indexing_service: props.indexing_service.clone(),
|
||||||
|
db_path: db_path().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration validation dialog
|
// 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::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;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum IndexingStatus {
|
pub enum IndexingStatus {
|
||||||
Idle,
|
Idle,
|
||||||
|
|
@ -149,6 +160,49 @@ impl IndexingService {
|
||||||
self.stop_indexing()
|
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
|
/// 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> {
|
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)
|
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 document_extraction;
|
||||||
mod indexing;
|
mod indexing;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod search;
|
||||||
|
|
||||||
// Global indexing service for signal handling
|
// Global indexing service for signal handling
|
||||||
static INDEXING_SERVICE: OnceLock<Arc<indexing::IndexingService>> = OnceLock::new();
|
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