-
-
Notifications
You must be signed in to change notification settings - Fork 691
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
845 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/target | ||
meilisearch | ||
data.ms | ||
dumps | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
[package] | ||
name = "meilisearch_searchbar" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
|
||
[lib] | ||
crate-type = ["cdylib", "rlib"] | ||
|
||
[dependencies] | ||
leptos = {version = "0.6.5",features = ["nightly"]} | ||
leptos_axum = { version = "0.6.5", optional = true} | ||
meilisearch-sdk = { version = "0.24.3", optional = true} | ||
axum = {version = "0.7.4", optional = true} | ||
leptos_meta = {version = "0.6.5",features = ["nightly"]} | ||
leptos_router = {version = "0.6.5",features = ["nightly"]} | ||
console_log = "1.0.0" | ||
console_error_panic_hook = "0.1.7" | ||
log = "0.4.20" | ||
tower = {verison= "0.4.13", optional=true} | ||
tower-http = {version = "0.5.1", optional = true, features = ["fs"]} | ||
simple_logger = {version = "4.3.3", optional = true} | ||
tokio = { version = "1", features = ["full"], optional = true } | ||
lazy_static = { version = "1.4.0", optional = true } | ||
serde = "1.0.196" | ||
serde_json = "1.0.113" | ||
csv = {version = "1.3.0", optional=true} | ||
|
||
[features] | ||
default = ["ssr"] | ||
hydrate = ["leptos/hydrate","leptos_meta/hydrate","leptos_router/hydrate"] | ||
ssr = [ | ||
"tokio", | ||
"lazy_static", | ||
"simple_logger", | ||
"dep:meilisearch-sdk", | ||
"dep:axum", | ||
"leptos/ssr", | ||
"leptos_meta/ssr", | ||
"leptos_router/ssr", | ||
"tower", | ||
"tower-http", | ||
"leptos_axum", | ||
"csv", | ||
] | ||
lazy_static = ["dep:lazy_static"] | ||
|
||
[package.metadata.leptos] | ||
output-name = "meilisearch_searchbar" | ||
site-root = "target/site" | ||
site-pkg-dir = "pkg" | ||
assets-dir = "public" | ||
site-addr = "127.0.0.1:3000" | ||
reload-port = 3001 | ||
browserquery = "defaults" | ||
watch = false | ||
env = "DEV" | ||
bin-features = ["ssr"] | ||
bin-default-features = false | ||
lib-features = ["hydrate"] | ||
lib-default-features = false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Meilisearch Searchbar | ||
|
||
This show how to integrate meilisearch with a leptos app, including a search bar and showing the results to the user. | ||
<br><br> | ||
We'll run meilisearch locally, as opposed to using their cloud service. | ||
<br><br> | ||
To get started install meilisearch into this example's root. | ||
|
||
```sh | ||
curl -L https://install.meilisearch.com | sh | ||
``` | ||
|
||
Run it. | ||
|
||
```sh | ||
./meilisearch | ||
``` | ||
|
||
Then set the environment variable and serve the app. I've included the address of my own local meilisearch server. | ||
I didn't provide a password to meilisearch during my setup, and I didn't provide one in my environment variables either. | ||
```sh | ||
MEILISEARCH_URL=http://localhost:7700 && cargo leptos serve | ||
``` | ||
|
||
Navigate to 127.0.0.1:3000 and start typing in popular American company names. (Boeing, Pepsi, etc) | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
use axum::{ | ||
body::Body, | ||
extract::State, | ||
http::{Request, Response, StatusCode, Uri}, | ||
response::{IntoResponse, Response as AxumResponse}, | ||
}; | ||
use leptos::{view, LeptosOptions}; | ||
use tower::ServiceExt; | ||
use tower_http::services::ServeDir; | ||
|
||
pub async fn file_and_error_handler( | ||
uri: Uri, | ||
State(options): State<LeptosOptions>, | ||
req: Request<Body>, | ||
) -> AxumResponse { | ||
let root = options.site_root.clone(); | ||
log::debug!("uri = {uri:?} root = {root} "); | ||
let res = get_static_file(uri.clone(), &root).await.unwrap(); | ||
|
||
if res.status() == StatusCode::OK { | ||
res.into_response() | ||
} else { | ||
let handler = leptos_axum::render_app_to_stream( | ||
options.to_owned(), | ||
|| view! {"Error! Error! Error!"}, | ||
); | ||
handler(req).await.into_response() | ||
} | ||
} | ||
|
||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { | ||
let req = Request::builder() | ||
.uri(uri.clone()) | ||
.body(Body::empty()) | ||
.unwrap(); | ||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` | ||
// This path is relative to the cargo root | ||
match ServeDir::new(root).oneshot(req).await { | ||
Ok(res) => Ok(res.into_response()), | ||
Err(err) => Err(( | ||
StatusCode::INTERNAL_SERVER_ERROR, | ||
format!("Something went wrong: {}", err), | ||
)), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
use leptos::*; | ||
use leptos_meta::*; | ||
use leptos_router::*; | ||
#[cfg(feature = "ssr")] | ||
pub mod fallback; | ||
|
||
#[cfg(feature = "hydrate")] | ||
#[wasm_bindgen::prelude::wasm_bindgen] | ||
pub fn hydrate() { | ||
// initializes logging using the `log` crate | ||
_ = console_log::init_with_level(log::Level::Debug); | ||
console_error_panic_hook::set_once(); | ||
|
||
leptos::mount_to_body(App); | ||
} | ||
|
||
#[component] | ||
pub fn App() -> impl IntoView { | ||
provide_meta_context(); | ||
// Provide this two our search components, they'll share a read and write handle to a Vec<StockRow>. | ||
let search_results = create_rw_signal(Vec::<StockRow>::new()); | ||
provide_context(search_results); | ||
view! { | ||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/> | ||
<Meta name="description" content="Leptos implementation of a Meilisearch backed Searchbar."/> | ||
<Router> | ||
<main> | ||
<Routes> | ||
<Route path="/" view=||view!{ | ||
<SearchBar/> | ||
<SearchResults/> | ||
}/> | ||
</Routes> | ||
</main> | ||
</Router> | ||
} | ||
} | ||
|
||
#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)] | ||
pub struct StockRow { | ||
id: u32, | ||
name: String, | ||
last: String, | ||
high: String, | ||
low: String, | ||
absolute_change: f32, | ||
percentage_change: f32, | ||
volume: u64, | ||
} | ||
|
||
#[leptos::server] | ||
pub async fn search_query(query: String) -> Result<Vec<StockRow>, ServerFnError> { | ||
use leptos_axum::extract; | ||
// Wow, so ergonomic! | ||
let axum::Extension::<meilisearch_sdk::Client>(client) = extract().await?; | ||
// Meilisearch has great defaults, lots of things are thought of for out of the box utility. | ||
// They limit the result length automatically (to 20), and have user friendly typo corrections and return similar words. | ||
let hits = client | ||
.get_index("stock_prices") | ||
.await | ||
.unwrap() | ||
.search() | ||
.with_query(query.as_str()) | ||
.execute::<StockRow>() | ||
.await | ||
.map_err(|err| ServerFnError::new(err.to_string()))? | ||
.hits; | ||
|
||
Ok(hits | ||
.into_iter() | ||
.map(|search_result| search_result.result) | ||
.collect()) | ||
} | ||
|
||
#[component] | ||
pub fn SearchBar() -> impl IntoView { | ||
let write_search_results = expect_context::<RwSignal<Vec<StockRow>>>().write_only(); | ||
let search_query = create_server_action::<SearchQuery>(); | ||
create_effect(move |_| { | ||
if let Some(value) = search_query.value()() { | ||
match value { | ||
Ok(search_results) => { | ||
write_search_results.set(search_results); | ||
} | ||
Err(err) => { | ||
leptos::logging::log!("{err}") | ||
} | ||
} | ||
} | ||
}); | ||
|
||
view! { | ||
<div> | ||
<label for="search">Search</label> | ||
<input id="search" on:input=move|e|{ | ||
let query = event_target_value(&e); | ||
search_query.dispatch(SearchQuery{query}); | ||
}/> | ||
</div> | ||
} | ||
} | ||
|
||
#[component] | ||
pub fn SearchResults() -> impl IntoView { | ||
let read_search_results = expect_context::<RwSignal<Vec<StockRow>>>().read_only(); | ||
view! { | ||
<ul> | ||
<For | ||
each=read_search_results | ||
key=|row| row.name.clone() | ||
children=move |StockRow{name,last,high,low,absolute_change,percentage_change,volume,..}: StockRow| { | ||
view! { | ||
<li> | ||
{format!("{name}; last: {last}; high: {high}; low: {low}; chg.: {absolute_change}; chg...:{percentage_change}; volume:{volume}")} | ||
</li> | ||
} | ||
} | ||
/> | ||
</ul> | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
#[cfg(feature = "ssr")] | ||
#[tokio::main] | ||
async fn main() { | ||
use axum::{routing::get, Extension, Router}; | ||
use leptos::get_configuration; | ||
use leptos_axum::{generate_route_list, LeptosRoutes}; | ||
use meilisearch_searchbar::StockRow; | ||
use meilisearch_searchbar::{fallback::file_and_error_handler, *}; | ||
|
||
// simple_logger is a lightweight alternative to tracing, when you absolutely have to trace, use tracing. | ||
simple_logger::SimpleLogger::new() | ||
.with_level(log::LevelFilter::Debug) | ||
.init() | ||
.unwrap(); | ||
|
||
let mut rdr = csv::Reader::from_path("data_set.csv").unwrap(); | ||
|
||
// Our data set doesn't have a good id for the purposes of meilisearch, Name is unique but it's not formatted correctly because it may have spaces. | ||
let documents: Vec<StockRow> = rdr | ||
.records() | ||
.enumerate() | ||
.map(|(i, rec)| { | ||
// There's probably a better way to do this. | ||
let mut record = csv::StringRecord::new(); | ||
record.push_field(&i.to_string()); | ||
for field in rec.unwrap().iter() { | ||
record.push_field(field); | ||
} | ||
record | ||
.deserialize::<StockRow>(None) | ||
.expect(&format!("{:?}", record)) | ||
}) | ||
.collect(); | ||
|
||
// My own check. I know how long I expect it to be, if it's not this length something is wrong. | ||
assert_eq!(documents.len(), 503); | ||
|
||
let client = meilisearch_sdk::Client::new( | ||
std::env::var("MEILISEARCH_URL").unwrap(), | ||
std::env::var("MEILISEARCH_API_KEY").ok(), | ||
); | ||
// An index is where the documents are stored. | ||
let task = client | ||
.create_index("stock_prices", Some("id")) | ||
.await | ||
.unwrap(); | ||
|
||
// Meilisearch may take some time to execute the request so we are going to wait till it's completed | ||
client.wait_for_task(task, None, None).await.unwrap(); | ||
|
||
let task_2 = client | ||
.get_index("stock_prices") | ||
.await | ||
.unwrap() | ||
.add_documents(&documents, Some("id")) | ||
.await | ||
.unwrap(); | ||
|
||
client.wait_for_task(task_2, None, None).await.unwrap(); | ||
|
||
drop(documents); | ||
|
||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); | ||
let leptos_options = conf.leptos_options; | ||
let addr = leptos_options.site_addr; | ||
let routes = generate_route_list(App); | ||
|
||
// build our application with a route | ||
let app = Router::new() | ||
.route("/favicon.ico", get(file_and_error_handler)) | ||
.leptos_routes(&leptos_options, routes, App) | ||
.fallback(file_and_error_handler) | ||
.layer(Extension(client)) | ||
.with_state(leptos_options); | ||
|
||
// run our app with hyper | ||
// `axum::Server` is a re-export of `hyper::Server` | ||
println!("listening on {}", addr); | ||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); | ||
axum::serve(listener, app.into_make_service()) | ||
.await | ||
.unwrap(); | ||
} |