Skip to content

Commit

Permalink
meilisearch_searchbar example
Browse files Browse the repository at this point in the history
  • Loading branch information
sjud committed Feb 14, 2024
1 parent d4bdc36 commit a726fe3
Show file tree
Hide file tree
Showing 7 changed files with 845 additions and 0 deletions.
5 changes: 5 additions & 0 deletions examples/meilisearch_searchbar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/target
meilisearch
data.ms
dumps
Cargo.lock
61 changes: 61 additions & 0 deletions examples/meilisearch_searchbar/Cargo.toml
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
26 changes: 26 additions & 0 deletions examples/meilisearch_searchbar/README.md
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)

504 changes: 504 additions & 0 deletions examples/meilisearch_searchbar/data_set.csv

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions examples/meilisearch_searchbar/src/fallback.rs
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),
)),
}
}
121 changes: 121 additions & 0 deletions examples/meilisearch_searchbar/src/lib.rs
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>
}
}
83 changes: 83 additions & 0 deletions examples/meilisearch_searchbar/src/main.rs
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();
}

0 comments on commit a726fe3

Please sign in to comment.