2020-08-05 19:52:27 +08:00
|
|
|
use std::collections::HashSet;
|
2020-07-11 20:17:37 +08:00
|
|
|
use std::fs::File;
|
2020-05-31 23:48:13 +08:00
|
|
|
use std::net::SocketAddr;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::time::Instant;
|
|
|
|
|
2020-07-11 20:17:37 +08:00
|
|
|
use askama_warp::Template;
|
2020-05-31 23:48:13 +08:00
|
|
|
use heed::EnvOpenOptions;
|
|
|
|
use serde::Deserialize;
|
|
|
|
use structopt::StructOpt;
|
|
|
|
use warp::{Filter, http::Response};
|
|
|
|
|
2020-08-31 03:50:30 +08:00
|
|
|
use milli::tokenizer::{simple_tokenizer, TokenType};
|
2020-08-13 20:15:05 +08:00
|
|
|
use milli::{Index, SearchResult};
|
2020-05-31 23:48:13 +08:00
|
|
|
|
2020-06-11 04:05:01 +08:00
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
#[global_allocator]
|
|
|
|
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
#[derive(Debug, StructOpt)]
|
2020-07-12 17:06:45 +08:00
|
|
|
#[structopt(name = "milli", about = "The server binary of the milli project.")]
|
2020-05-31 23:48:13 +08:00
|
|
|
struct Opt {
|
|
|
|
/// The database path where the LMDB database is located.
|
|
|
|
/// It is created if it doesn't already exist.
|
|
|
|
#[structopt(long = "db", parse(from_os_str))]
|
|
|
|
database: PathBuf,
|
|
|
|
|
|
|
|
/// The maximum size the database can take on disk. It is recommended to specify
|
|
|
|
/// the whole disk space (value must be a multiple of a page size).
|
|
|
|
#[structopt(long = "db-size", default_value = "107374182400")] // 100 GB
|
|
|
|
database_size: usize,
|
|
|
|
|
2020-07-14 17:27:46 +08:00
|
|
|
/// Disable document highlighting on the dashboard.
|
|
|
|
#[structopt(long)]
|
|
|
|
disable_highlighting: bool,
|
|
|
|
|
2020-07-12 17:04:35 +08:00
|
|
|
/// Verbose mode (-v, -vv, -vvv, etc.)
|
|
|
|
#[structopt(short, long, parse(from_occurrences))]
|
|
|
|
verbose: usize,
|
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
/// The ip and port on which the database will listen for HTTP requests.
|
|
|
|
#[structopt(short = "l", long, default_value = "127.0.0.1:9700")]
|
|
|
|
http_listen_addr: String,
|
|
|
|
}
|
|
|
|
|
2020-08-31 20:20:42 +08:00
|
|
|
fn highlight_record(record: &csv::StringRecord, words: &HashSet<String>) -> csv::StringRecord {
|
|
|
|
let mut output_record = csv::StringRecord::new();
|
|
|
|
let mut buffer = String::new();
|
|
|
|
for field in record {
|
|
|
|
buffer.clear();
|
|
|
|
for (token_type, token) in simple_tokenizer(field) {
|
|
|
|
if token_type == TokenType::Word {
|
|
|
|
let lowercase_token = token.to_lowercase();
|
|
|
|
let to_highlight = words.contains(&lowercase_token);
|
|
|
|
if to_highlight { buffer.push_str("<mark>") }
|
|
|
|
buffer.push_str(token);
|
|
|
|
if to_highlight { buffer.push_str("</mark>") }
|
|
|
|
} else {
|
|
|
|
buffer.push_str(token);
|
|
|
|
}
|
2020-08-31 03:50:30 +08:00
|
|
|
}
|
2020-08-31 20:20:42 +08:00
|
|
|
output_record.push_field(&buffer);
|
2020-08-05 19:52:27 +08:00
|
|
|
}
|
2020-08-31 20:20:42 +08:00
|
|
|
output_record
|
2020-08-05 19:52:27 +08:00
|
|
|
}
|
|
|
|
|
2020-07-11 20:17:37 +08:00
|
|
|
#[derive(Template)]
|
|
|
|
#[template(path = "index.html")]
|
|
|
|
struct IndexTemplate {
|
|
|
|
db_name: String,
|
|
|
|
db_size: usize,
|
|
|
|
docs_count: usize,
|
|
|
|
}
|
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> anyhow::Result<()> {
|
|
|
|
let opt = Opt::from_args();
|
|
|
|
|
2020-07-12 17:04:35 +08:00
|
|
|
stderrlog::new()
|
|
|
|
.verbosity(opt.verbose)
|
|
|
|
.show_level(false)
|
|
|
|
.timestamp(stderrlog::Timestamp::Off)
|
|
|
|
.init()?;
|
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
let env = EnvOpenOptions::new()
|
|
|
|
.map_size(opt.database_size)
|
|
|
|
.max_dbs(10)
|
|
|
|
.open(&opt.database)?;
|
|
|
|
|
2020-08-07 19:11:31 +08:00
|
|
|
// Open the LMDB database.
|
2020-08-28 21:38:05 +08:00
|
|
|
let index = Index::new(&env)?;
|
2020-08-07 19:11:31 +08:00
|
|
|
|
2020-07-11 20:51:59 +08:00
|
|
|
// Retrieve the database the file stem (w/o the extension),
|
|
|
|
// the disk file size and the number of documents in the database.
|
2020-07-11 20:17:37 +08:00
|
|
|
let db_name = opt.database.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_string();
|
|
|
|
let db_size = File::open(opt.database.join("data.mdb"))?.metadata()?.len() as usize;
|
2020-08-28 21:38:05 +08:00
|
|
|
|
|
|
|
let rtxn = env.read_txn()?;
|
|
|
|
let docs_count = index.number_of_documents(&rtxn)? as usize;
|
|
|
|
drop(rtxn);
|
2020-07-11 20:17:37 +08:00
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
// We run and wait on the HTTP server
|
|
|
|
|
|
|
|
// Expose an HTML page to debug the search in a browser
|
|
|
|
let dash_html_route = warp::filters::method::get()
|
|
|
|
.and(warp::filters::path::end())
|
2020-08-28 21:38:05 +08:00
|
|
|
.map(move || IndexTemplate { db_name: db_name.clone(), db_size, docs_count });
|
2020-05-31 23:48:13 +08:00
|
|
|
|
|
|
|
let dash_bulma_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("bulma.min.css"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "text/css; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/bulma.min.css"))
|
|
|
|
);
|
|
|
|
|
2020-07-14 05:51:41 +08:00
|
|
|
let dash_bulma_dark_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("bulma-prefers-dark.min.css"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "text/css; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/bulma-prefers-dark.min.css"))
|
|
|
|
);
|
|
|
|
|
2020-07-11 17:48:27 +08:00
|
|
|
let dash_style_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("style.css"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "text/css; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/style.css"))
|
|
|
|
);
|
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
let dash_jquery_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("jquery-3.4.1.min.js"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "application/javascript; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/jquery-3.4.1.min.js"))
|
|
|
|
);
|
|
|
|
|
|
|
|
let dash_papaparse_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("papaparse.min.js"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "application/javascript; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/papaparse.min.js"))
|
|
|
|
);
|
|
|
|
|
2020-07-11 20:17:37 +08:00
|
|
|
let dash_filesize_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("filesize.min.js"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "application/javascript; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/filesize.min.js"))
|
|
|
|
);
|
|
|
|
|
2020-07-11 17:48:27 +08:00
|
|
|
let dash_script_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("script.js"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "application/javascript; charset=utf-8")
|
|
|
|
.body(include_str!("../../public/script.js"))
|
|
|
|
);
|
|
|
|
|
2020-07-16 05:51:12 +08:00
|
|
|
let dash_logo_white_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("logo-white.svg"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "image/svg+xml")
|
|
|
|
.body(include_str!("../../public/logo-white.svg"))
|
|
|
|
);
|
|
|
|
|
|
|
|
let dash_logo_black_route = warp::filters::method::get()
|
|
|
|
.and(warp::path!("logo-black.svg"))
|
|
|
|
.map(|| Response::builder()
|
|
|
|
.header("content-type", "image/svg+xml")
|
|
|
|
.body(include_str!("../../public/logo-black.svg"))
|
|
|
|
);
|
|
|
|
|
2020-05-31 23:48:13 +08:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct QueryBody {
|
2020-10-06 20:52:05 +08:00
|
|
|
query: Option<String>,
|
2020-05-31 23:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
let env_cloned = env.clone();
|
2020-07-14 17:27:46 +08:00
|
|
|
let disable_highlighting = opt.disable_highlighting;
|
2020-05-31 23:48:13 +08:00
|
|
|
let query_route = warp::filters::method::post()
|
|
|
|
.and(warp::path!("query"))
|
|
|
|
.and(warp::body::json())
|
|
|
|
.map(move |query: QueryBody| {
|
|
|
|
let before_search = Instant::now();
|
|
|
|
let rtxn = env_cloned.read_txn().unwrap();
|
|
|
|
|
2020-10-06 20:52:05 +08:00
|
|
|
let mut search = index.search(&rtxn);
|
|
|
|
if let Some(query) = query.query {
|
|
|
|
search.query(query);
|
|
|
|
}
|
|
|
|
|
|
|
|
let SearchResult { found_words, documents_ids } = search.execute().unwrap();
|
2020-05-31 23:48:13 +08:00
|
|
|
|
2020-08-31 20:20:42 +08:00
|
|
|
let body = match index.headers(&rtxn).unwrap() {
|
|
|
|
Some(headers) => {
|
|
|
|
let mut wtr = csv::Writer::from_writer(Vec::new());
|
|
|
|
|
|
|
|
// We write the headers
|
|
|
|
wtr.write_record(&headers).unwrap();
|
|
|
|
|
|
|
|
let documents = index.documents(&rtxn, documents_ids).unwrap();
|
|
|
|
for (_id, record) in documents {
|
|
|
|
let record = if disable_highlighting {
|
|
|
|
record
|
|
|
|
} else {
|
|
|
|
highlight_record(&record, &found_words)
|
|
|
|
};
|
|
|
|
wtr.write_record(&record).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
wtr.into_inner().unwrap()
|
|
|
|
},
|
|
|
|
None => Vec::new(),
|
|
|
|
};
|
2020-05-31 23:48:13 +08:00
|
|
|
|
|
|
|
Response::builder()
|
|
|
|
.header("Content-Type", "text/csv")
|
|
|
|
.header("Time-Ms", before_search.elapsed().as_millis().to_string())
|
|
|
|
.body(String::from_utf8(body).unwrap())
|
|
|
|
});
|
|
|
|
|
|
|
|
let routes = dash_html_route
|
|
|
|
.or(dash_bulma_route)
|
2020-07-14 05:51:41 +08:00
|
|
|
.or(dash_bulma_dark_route)
|
2020-07-11 17:48:27 +08:00
|
|
|
.or(dash_style_route)
|
2020-05-31 23:48:13 +08:00
|
|
|
.or(dash_jquery_route)
|
|
|
|
.or(dash_papaparse_route)
|
2020-07-11 20:17:37 +08:00
|
|
|
.or(dash_filesize_route)
|
2020-07-11 17:48:27 +08:00
|
|
|
.or(dash_script_route)
|
2020-07-16 05:51:12 +08:00
|
|
|
.or(dash_logo_white_route)
|
|
|
|
.or(dash_logo_black_route)
|
2020-05-31 23:48:13 +08:00
|
|
|
.or(query_route);
|
|
|
|
|
|
|
|
let addr = SocketAddr::from_str(&opt.http_listen_addr).unwrap();
|
|
|
|
warp::serve(routes).run(addr).await;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|