Skip to content

Commit 59e4630

Browse files
committed
Initial Implementation of multithreading
1 parent de08ab2 commit 59e4630

3 files changed

Lines changed: 150 additions & 54 deletions

File tree

src/main.rs

Lines changed: 97 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std:: {
2-
env::{self}, fs::{self, File}, io::{self, BufReader, BufWriter, Read}, path::Path, process::{exit, ExitCode}, str::{self}
2+
env::{self}, fs::{self, File}, io::{self, BufReader, BufWriter, Read}, path::Path, process::{exit, ExitCode}, str::{self},
3+
sync::{Arc, Mutex}, thread
34
};
45

56
use xml::{self, reader::XmlEvent, EventReader};
@@ -10,7 +11,7 @@ mod lexer;
1011
mod server;
1112
mod model;
1213

13-
use crate::model::{InMemoryModel, Model, SqliteModel};
14+
use crate::model::*;
1415

1516
/* Parse all the text (Character Events) from the XML File */
1617
fn parse_xml_file(file_path: &Path) -> Result<String, ()> {
@@ -36,6 +37,7 @@ fn parse_xml_file(file_path: &Path) -> Result<String, ()> {
3637
Ok(content)
3738
}
3839

40+
/* Parse all the text from the TXT File */
3941
fn parse_txt_file(file_path: &Path) -> Result<String, ()> {
4042
let mut content = String::new();
4143
let mut file = File::open(file_path).map_err(|err| {
@@ -81,30 +83,37 @@ fn save_model_as_json(model: &InMemoryModel, index_path: &str) -> Result<(), ()>
8183
Ok(())
8284
}
8385

84-
/* Indexes a folder as a json file and adds to model */
85-
fn append_folder_to_model(dir_path: &Path, model: &mut dyn Model) -> Result<(), ()> {
86+
/* Indexes a folder as a json file and adds to model, Processed is the number of file indexed */
87+
fn append_folder_to_model(dir_path: &Path, model: Arc<Mutex<InMemoryModel>>, processed: &mut usize) -> Result<(), ()> {
8688
let dir = fs::read_dir(dir_path).map_err(|err| {
87-
eprintln!("{err}: Failed to read directory {dir_path} as \"{msg}\"", err = "ERROR".bold().red(),
89+
eprintln!("{}: Failed to read directory {dir_path} as \"{err}\"", "ERROR".bold().red(),
8890
dir_path = dir_path.to_str().unwrap().bold().bright_blue(),
89-
msg = err.to_string().red());
91+
err = err.to_string().red());
9092
})?;
9193

9294
'step: for file in dir {
9395
let file = file.map_err(|err| {
94-
eprintln!("{err}: Failed to read next file in directory {dir_path} as {msg}", err = "ERROR".bold().red(),
96+
eprintln!("{}: Failed to read next file in directory {dir_path} as {err}", "ERROR".bold().red(),
9597
dir_path = dir_path.to_str().unwrap().bold().bright_blue(),
96-
msg = err.to_string().red());
98+
err = err.to_string().red());
9799
})?;
98100

99101
let file_path = file.path();
100102
let file_path_str = file_path.to_str().unwrap();
103+
104+
let last_modified = file.metadata().map_err(|err| {
105+
eprintln!("{}: Failed to get metadata of file {path} as {err}", "ERROR".bold().red(), path = file_path_str.bright_blue(), err = err.to_string().red());
106+
})?.modified().map_err(|err| {
107+
eprintln!("{}: Failed to last modified time of file {path} as {err}", "ERROR".bold().red(), path = file_path_str.bright_blue(), err = err.to_string().red());
108+
}).unwrap();
109+
101110
let file_ext = file_path.extension();
102111

103112
// Skip unsupported files
104113
if let Some(ext) = file_ext {
105114
const ALLOWED_EXTS: [&str; 4] = ["xml", "xhtml", "txt", "md"];
106115
if !ALLOWED_EXTS.contains(&ext.to_str().unwrap()) {
107-
println!("{}: Skipping non-XML file {}", "INFO".cyan(), file_path_str.bright_yellow());
116+
println!("{}: Skipping unsupported file {}", "INFO".cyan(), file_path_str.bright_yellow());
108117
continue 'step;
109118
}
110119
}
@@ -116,22 +125,33 @@ fn append_folder_to_model(dir_path: &Path, model: &mut dyn Model) -> Result<(),
116125

117126
// Recursively index all the folders
118127
if file_type.is_dir() {
119-
append_folder_to_model(&file_path, model)?;
128+
// Skip the .git folder
129+
if file_path.file_name().unwrap() == ".git" {
130+
continue 'step;
131+
}
132+
133+
append_folder_to_model(&file_path, Arc::clone(&model), processed)?;
120134
continue 'step;
121135
}
122136

123-
println!("{}: Indexing {} ...", "INFO".cyan(), file_path_str.bright_cyan());
137+
let mut model = model.lock().unwrap();
138+
if model.requires_reindexing(&file_path, last_modified)? {
139+
println!("{}: Indexing {} ...", "INFO".cyan(), file_path_str.bright_cyan());
140+
141+
let content = match parse_file_by_ext(&file_path) {
142+
Ok(content) => content.chars().collect::<Vec<_>>(),
143+
Err(_) => {
144+
eprintln!("{}: Failed to read xml file {path}", "ERROR".bold().red(), path = file_path.to_str().unwrap().bright_blue());
145+
continue 'step;
146+
}
147+
};
148+
149+
model.add_document(file_path, &content, last_modified)?;
150+
*processed += 1;
151+
} else {
152+
println!("{}: Ignoring {} as already indexed ...", "INFO".cyan(), file_path_str.bright_cyan());
153+
}
124154

125-
let content = match parse_file_by_ext(&file_path) {
126-
Ok(content) => content.chars().collect::<Vec<_>>(),
127-
Err(err) => {
128-
eprintln!("{error}: Failed to read xml file {fp}: {msg:?}", error = "ERROR".bold().red(), fp = file_path.to_str().unwrap().bright_blue(), msg = err);
129-
continue 'step;
130-
}
131-
};
132-
133-
// Core Operation
134-
model.add_document(file_path, &content)?;
135155
}
136156
Ok(())
137157
}
@@ -155,13 +175,13 @@ fn check_index(index_path: &str) -> Result<(), ()> {
155175
const DEFAULT_INDEX_JSON_PATH: &str = "index.json";
156176
const DEFAULT_INDEX_SQLITE_DB_PATH: &str = "index.db";
157177

158-
178+
/* Fetch the InMemory model from an index file */
159179
fn fetch_model(index_path: &str) -> Result<InMemoryModel, ()> {
160180
let index_file = fs::File::open(&index_path).map_err(|err| {
161181
eprintln!("{}: Could not open file {file_path} as \"{err}\"", "ERROR".bold().red(), file_path = index_path.bright_blue(), err = err.to_string().red());
162182
}).unwrap();
163183

164-
let model: InMemoryModel = serde_json::from_reader(BufReader::new(index_file)).map_err(|err| {
184+
let model = serde_json::from_reader(BufReader::new(index_file)).map_err(|err| {
165185
eprintln!("{}: Serde failed to read {file_path} as \"{err}\"", "ERROR".bold().red(), file_path = index_path.bright_blue(), err = err.to_string().red());
166186
}).unwrap();
167187

@@ -200,6 +220,27 @@ fn entry() -> Result<(), ()> {
200220
})?;
201221

202222
match subcommand.as_str() {
223+
/*
224+
"reindex" => {
225+
assert!(!use_sqlite_mode, "The sqlite model is deprecated");
226+
let dir_path = args.next().ok_or_else(|| {
227+
usage(&program);
228+
eprintln!("ERROR: no directory is provided for {subcommand} subcommand");
229+
})?;
230+
231+
let index_path = "index.json";
232+
let index_file = File::open(&index_path).map_err(|err| {
233+
eprintln!("ERROR: could not open index file {index_path}: {err}");
234+
})?;
235+
let mut model: InMemoryModel = serde_json::from_reader(index_file).map_err(|err| {
236+
eprintln!("ERROR: could not parse index file {index_path}: {err}");
237+
})?;
238+
239+
append_folder_to_model(Path::new(&dir_path), &mut model)?;
240+
save_model_as_json(&model, index_path)?;
241+
return Ok(());
242+
}
243+
203244
"index" => {
204245
let dir_path = args.next().ok_or_else(|| {
205246
usage(&program);
@@ -224,13 +265,15 @@ fn entry() -> Result<(), ()> {
224265
225266
} else {
226267
let index_path = "index.json";
227-
let mut model = InMemoryModel::default();
268+
let mut model = Default::default();
228269
append_folder_to_model(Path::new(&dir_path), &mut model)?;
229270
save_model_as_json(&model, index_path)?;
230271
}
231272
}
273+
*/
232274

233275
"search" => {
276+
assert!(!use_sqlite_mode, "Sqlite mode is DEPRACATED.");
234277
let index_path = args.next().ok_or_else(|| {
235278
usage(&program);
236279
eprintln!("{}: Index file path must provided for {} subcommand.", "ERROR".bold().red(), subcommand.bold().bright_blue());
@@ -269,25 +312,44 @@ fn entry() -> Result<(), ()> {
269312
}
270313

271314
"serve" => {
272-
let index_path = args.next().ok_or_else(|| {
315+
assert!(!use_sqlite_mode, "SQLITE mode is DEPRACATED.");
316+
let dir_path = args.next().ok_or_else(|| {
273317
usage(&program);
274-
eprintln!("{}: Index file path must provided for {} subcommand.", "ERROR".bold().red(), subcommand.bold().bright_blue());
318+
eprintln!("{}: No directory path is provided for {} subcommand.", "ERROR".bold().red(), subcommand.bold().bright_blue());
275319
})?;
276-
320+
277321
// Default address
278322
let address = args.next().unwrap_or("127.0.0.1:6969".to_string());
279323

280-
if use_sqlite_mode {
281-
let model = SqliteModel::open(Path::new(&index_path))?;
282-
return server::start(&address, &model);
283-
284-
} else {
285-
let model: InMemoryModel = fetch_model(&index_path).unwrap_or_else(|()| {
286-
eprintln!("{}: Failed to fetch model for {}.", "ERROR".bold().red(), index_path.bright_blue());
324+
// TODO: Figure out index_path based on dir_path
325+
let index_path = DEFAULT_INDEX_JSON_PATH;
326+
let exists = Path::new(index_path).exists();
327+
328+
let model: Arc<Mutex<InMemoryModel>>;
329+
if exists {
330+
// Fetch already existing model
331+
model = Arc::new(Mutex::new(fetch_model(&DEFAULT_INDEX_JSON_PATH).unwrap_or_else(|()| {
332+
eprintln!("{}: Failed to fetch model for {}.", "ERROR".bold().red(), DEFAULT_INDEX_JSON_PATH.bright_blue());
287333
exit(1);
334+
})));
335+
} else {
336+
model = Arc::new(Mutex::new(Default::default()));
337+
}
338+
339+
{
340+
let model = Arc::clone(&model);
341+
thread::spawn(move || {
342+
let mut processed: usize = 0 as usize;
343+
append_folder_to_model(Path::new(&dir_path), Arc::clone(&model), &mut processed).unwrap();
344+
345+
// Save the model only when some files are processed
346+
if processed > 0 {
347+
let model= model.lock().unwrap();
348+
save_model_as_json(&model, index_path).unwrap();
349+
}
288350
});
289-
return server::start(&address, &model);
290351
}
352+
return server::start(&address, Arc::clone(&model));
291353
}
292354

293355
_ => {

src/model.rs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
path::{Path, PathBuf}
2+
path::{self, Path, PathBuf}, time::SystemTime
33
};
44

55
use std::collections::HashMap;
@@ -8,12 +8,12 @@ use std::default::Default;
88
use super::lexer::*;
99

1010
use sqlite::{self};
11-
use colored::Colorize;
11+
use colored::{Colorize};
1212

13-
// ---- Sqlite based Model Implementation ----
1413
pub trait Model {
1514
fn search_query(&self, query: &[char]) -> Result<Vec<(PathBuf, f32)>, ()>;
16-
fn add_document(&mut self, path: PathBuf, content: &[char]) -> Result<(), ()>;
15+
fn add_document(&mut self, path: PathBuf, content: &[char], last_modified: SystemTime) -> Result<(), ()>;
16+
fn requires_reindexing(&mut self, path: &Path, last_modified: SystemTime) -> Result<bool, ()>;
1717
}
1818

1919
pub struct SqliteModel {
@@ -97,11 +97,16 @@ impl SqliteModel {
9797
}
9898

9999
impl Model for SqliteModel {
100-
fn search_query(&self, query: &[char]) -> Result<Vec<(PathBuf, f32)>, ()> {
100+
fn search_query(&self, _query: &[char]) -> Result<Vec<(PathBuf, f32)>, ()> {
101101
todo!("SqliteModel::search_query()");
102102
}
103+
104+
fn requires_reindexing(&mut self, _path: &Path, _last_modified: SystemTime) -> Result<bool, ()> {
105+
Ok(true)
106+
}
107+
103108

104-
fn add_document(&mut self, path: PathBuf, content: &[char]) -> Result<(), ()> {
109+
fn add_document(&mut self, path: PathBuf, content: &[char], _last_modified: SystemTime) -> Result<(), ()> {
105110
let terms = Lexer::new(content).collect::<Vec<_>>();
106111
// Populate Documents Table
107112
let doc_id = {
@@ -196,18 +201,19 @@ pub type FreqTable = HashMap::<String, usize>;
196201
Map of term with frequency of occurence in all corpus of documents.*/
197202
pub type GlobalTermFreq = HashMap::<String, usize>;
198203

199-
#[derive(Default, Serialize, Deserialize)]
204+
#[derive(Serialize, Deserialize)]
200205
pub struct Doc {
201206
count: usize,
202-
ft: FreqTable
207+
ft: FreqTable,
208+
last_modified: SystemTime
203209
}
204210

205-
type Docs = HashMap::<PathBuf, Doc>;
211+
pub type Docs = HashMap::<PathBuf, Doc>;
206212

207213
#[derive(Default, Deserialize, Serialize)]
208214
pub struct InMemoryModel {
209215
pub gtf: GlobalTermFreq,
210-
pub docs: Docs
216+
pub docs: Docs,
211217
}
212218

213219
fn compute_tf(term: &str, doc: &Doc) -> f32 {
@@ -224,6 +230,20 @@ fn compute_idf(term: &str, model: &InMemoryModel) -> f32 {
224230
f32::log10(n / d)
225231
}
226232

233+
impl InMemoryModel {
234+
fn remove_document(&mut self, file_path: &Path) {
235+
// Remove the doc from docs
236+
if let Some(doc) = self.docs.remove(file_path) {
237+
for term in doc.ft.keys() {
238+
// Update the GlobalTermFrequency table
239+
if let Some(freq) = self.gtf.get_mut(term) {
240+
*freq -= 1;
241+
}
242+
}
243+
}
244+
}
245+
}
246+
227247
impl Model for InMemoryModel {
228248
fn search_query(&self, query: &[char]) -> Result<Vec<(PathBuf, f32)>, ()> {
229249
let mut results = Vec::new();
@@ -244,7 +264,10 @@ impl Model for InMemoryModel {
244264
Ok(results)
245265
}
246266

247-
fn add_document(&mut self, path: PathBuf, content: &[char]) -> Result<(), ()> {
267+
fn add_document(&mut self, file_path: PathBuf, content: &[char], last_modified: SystemTime) -> Result<(), ()> {
268+
// Remove earlier document
269+
self.remove_document(&file_path);
270+
248271
// Precompute all the tokens at once
249272
let tokens = Lexer::new(&content).collect::<Vec<_>>();
250273
let mut ft = FreqTable::new();
@@ -259,7 +282,17 @@ impl Model for InMemoryModel {
259282
for term in ft.keys() {
260283
self.gtf.entry(term.to_owned()).and_modify(|x| *x += 1).or_insert(1);
261284
}
262-
self.docs.insert(path, Doc { count: term_count, ft: ft });
285+
self.docs.insert(file_path, Doc { count: term_count, ft: ft , last_modified: last_modified});
263286
Ok(())
264287
}
265-
}
288+
289+
fn requires_reindexing(&mut self, file_path: &Path, last_modified: SystemTime) -> Result<bool, ()> {
290+
if let Some(doc) = self.docs.get(file_path) {
291+
return Ok(doc.last_modified < last_modified);
292+
}
293+
return Ok(true);
294+
}
295+
296+
}
297+
298+
// TODO: Implement a efficient sqlite Model with parellel processing support

0 commit comments

Comments
 (0)