Search not working
This commit is contained in:
parent
6d487dd5c7
commit
427a6cb597
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
/migration/target
|
/migration/target
|
||||||
node_modules
|
node_modules
|
||||||
|
/indices
|
||||||
|
@ -6,6 +6,26 @@ use tantivy::query::QueryParser;
|
|||||||
use tantivy::{doc, Index, IndexWriter, ReloadPolicy, Searcher};
|
use tantivy::{doc, Index, IndexWriter, ReloadPolicy, Searcher};
|
||||||
use tantivy::{schema::*, TantivyError};
|
use tantivy::{schema::*, TantivyError};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, derive_more::Deref)]
|
||||||
|
pub struct Search(pub Addr<SearchEngine>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RecipeRecord {
|
||||||
|
pub id: u64,
|
||||||
|
pub title: String,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::entities::recipies::Model> for RecipeRecord {
|
||||||
|
fn from(value: crate::entities::recipies::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id as u64,
|
||||||
|
title: value.title,
|
||||||
|
summary: value.summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Inner {
|
pub struct Inner {
|
||||||
writer: IndexWriter,
|
writer: IndexWriter,
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
@ -18,13 +38,17 @@ pub struct SearchEngine(Arc<Mutex<Inner>>);
|
|||||||
impl SearchEngine {
|
impl SearchEngine {
|
||||||
pub fn build() -> Result<Self, tantivy::TantivyError> {
|
pub fn build() -> Result<Self, tantivy::TantivyError> {
|
||||||
let index_path = std::path::Path::new("./indices");
|
let index_path = std::path::Path::new("./indices");
|
||||||
|
std::fs::create_dir_all(&index_path).expect("Failed to create indices directory");
|
||||||
|
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
schema_builder.add_u64_field("id", INDEXED);
|
schema_builder.add_u64_field("id", INDEXED);
|
||||||
schema_builder.add_text_field("title", TEXT);
|
schema_builder.add_text_field("title", TEXT);
|
||||||
schema_builder.add_text_field("summary", TEXT);
|
schema_builder.add_text_field("summary", TEXT);
|
||||||
|
|
||||||
let schema = schema_builder.build();
|
let schema = schema_builder.build();
|
||||||
let index = Index::create_in_dir(&index_path, schema.clone())?;
|
let index = Index::create_in_dir(&index_path, schema.clone())
|
||||||
|
.or_else(|_| Index::open_in_dir(&index_path))
|
||||||
|
.expect("Failed to construct indices directory");
|
||||||
let index_writer: IndexWriter = index.writer(50_000_000)?;
|
let index_writer: IndexWriter = index.writer(50_000_000)?;
|
||||||
|
|
||||||
let reader = index
|
let reader = index
|
||||||
@ -50,9 +74,7 @@ impl actix::Actor for SearchEngine {
|
|||||||
#[derive(Debug, Message)]
|
#[derive(Debug, Message)]
|
||||||
#[rtype(result = "Result<u64,TantivyError>")]
|
#[rtype(result = "Result<u64,TantivyError>")]
|
||||||
pub struct CreateRecipe {
|
pub struct CreateRecipe {
|
||||||
id: u64,
|
pub record: RecipeRecord,
|
||||||
title: String,
|
|
||||||
summary: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler<CreateRecipe> for SearchEngine {
|
impl Handler<CreateRecipe> for SearchEngine {
|
||||||
@ -68,10 +90,11 @@ impl Handler<CreateRecipe> for SearchEngine {
|
|||||||
let title = shared.schema.get_field("summary").unwrap();
|
let title = shared.schema.get_field("summary").unwrap();
|
||||||
let summary = shared.schema.get_field("summary").unwrap();
|
let summary = shared.schema.get_field("summary").unwrap();
|
||||||
|
|
||||||
|
let msg = msg.record;
|
||||||
let n = shared.writer.add_document(doc! {
|
let n = shared.writer.add_document(doc! {
|
||||||
id => msg.id,
|
id => msg.id,
|
||||||
title => msg.title,
|
title => msg.title,
|
||||||
summary => msg.summary,
|
summary => msg.summary.unwrap_or_default(),
|
||||||
})?;
|
})?;
|
||||||
shared.writer.commit()?;
|
shared.writer.commit()?;
|
||||||
|
|
||||||
@ -85,7 +108,7 @@ impl Handler<CreateRecipe> for SearchEngine {
|
|||||||
#[derive(Debug, Message)]
|
#[derive(Debug, Message)]
|
||||||
#[rtype(result = "Result<Vec<u64>,TantivyError>")]
|
#[rtype(result = "Result<Vec<u64>,TantivyError>")]
|
||||||
pub struct Find {
|
pub struct Find {
|
||||||
query: String,
|
pub query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler<Find> for SearchEngine {
|
impl Handler<Find> for SearchEngine {
|
||||||
@ -102,19 +125,38 @@ impl Handler<Find> for SearchEngine {
|
|||||||
let summary = shared.schema.get_field("summary").unwrap();
|
let summary = shared.schema.get_field("summary").unwrap();
|
||||||
|
|
||||||
let query_parser = QueryParser::for_index(&shared.index, vec![title, summary]);
|
let query_parser = QueryParser::for_index(&shared.index, vec![title, summary]);
|
||||||
let query = query_parser.parse_query(&msg.query)?;
|
let query = msg
|
||||||
|
.query
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|piece| {
|
||||||
|
piece
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphabetic())
|
||||||
|
.collect::<String>()
|
||||||
|
})
|
||||||
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" OR ");
|
||||||
|
tracing::debug!("Query is: {query:?}");
|
||||||
|
let query = query_parser.parse_query(&query).expect("invalid query");
|
||||||
|
|
||||||
let rows = shared.searcher.search(&query, &TopDocs::with_limit(100))?;
|
let rows = shared.searcher.search(&query, &TopDocs::with_limit(100))?;
|
||||||
|
|
||||||
let ids = rows
|
let ids = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|row| {
|
.filter_map(|row| {
|
||||||
|
tracing::debug!("tantivy row: {row:?}");
|
||||||
let doc: Option<TantivyDocument> = shared.searcher.doc(row.1).ok();
|
let doc: Option<TantivyDocument> = shared.searcher.doc(row.1).ok();
|
||||||
doc
|
doc
|
||||||
})
|
})
|
||||||
.fold(Vec::with_capacity(1_000), |agg, doc| {
|
.fold(Vec::with_capacity(1_000), |agg, doc| {
|
||||||
|
let json = doc.to_json(&shared.schema);
|
||||||
|
tracing::debug!("tantivy doc: {doc:?} {json:?}");
|
||||||
doc.get_all(id)
|
doc.get_all(id)
|
||||||
.filter_map(|id| id.as_u64())
|
.filter_map(|id| {
|
||||||
|
tracing::debug!("tantivy id: {id:?}");
|
||||||
|
id.as_u64()
|
||||||
|
})
|
||||||
.fold(agg, |mut agg, id| {
|
.fold(agg, |mut agg, id| {
|
||||||
agg.push(id);
|
agg.push(id);
|
||||||
agg
|
agg
|
||||||
@ -127,3 +169,41 @@ impl Handler<Find> for SearchEngine {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Message)]
|
||||||
|
#[rtype(result = "Result<(), TantivyError>")]
|
||||||
|
pub struct Refresh {
|
||||||
|
pub records: Vec<RecipeRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<Refresh> for SearchEngine {
|
||||||
|
type Result = actix::ResponseActFuture<Self, Result<(), TantivyError>>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: Refresh, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let inner = self.0.clone();
|
||||||
|
Box::pin(
|
||||||
|
async move {
|
||||||
|
let mut shared = inner.lock().unwrap();
|
||||||
|
|
||||||
|
let id = shared.schema.get_field("id").unwrap();
|
||||||
|
let title = shared.schema.get_field("summary").unwrap();
|
||||||
|
let summary = shared.schema.get_field("summary").unwrap();
|
||||||
|
|
||||||
|
shared.writer.delete_all_documents()?;
|
||||||
|
|
||||||
|
for msg in msg.records {
|
||||||
|
tracing::debug!("creating search index for {msg:?}");
|
||||||
|
let _n = shared.writer.add_document(doc! {
|
||||||
|
id => msg.id,
|
||||||
|
title => msg.title,
|
||||||
|
summary => msg.summary.unwrap_or_default(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
shared.writer.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.into_actor(self),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
57
src/main.rs
57
src/main.rs
@ -1,3 +1,4 @@
|
|||||||
|
use actix::Actor;
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
|
||||||
@ -15,6 +16,30 @@ pub mod types;
|
|||||||
|
|
||||||
const SESSION_KEY: &'static str = "session";
|
const SESSION_KEY: &'static str = "session";
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct Q {
|
||||||
|
q: String,
|
||||||
|
}
|
||||||
|
#[actix_web::get("/s")]
|
||||||
|
async fn test_search(
|
||||||
|
q: actix_web::web::Query<Q>,
|
||||||
|
search: Data<crate::actors::search::Search>,
|
||||||
|
) -> actix_web::HttpResponse {
|
||||||
|
let res = search
|
||||||
|
.send(crate::actors::search::Find {
|
||||||
|
query: q.into_inner().q,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tracing::debug!("search res: {res:?}");
|
||||||
|
actix_web::HttpResponse::Ok().body(
|
||||||
|
serde_json::to_string(&match res {
|
||||||
|
Ok(Ok(res)) => res,
|
||||||
|
_ => Vec::new(),
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let _ = tracing_subscriber::fmt::init();
|
let _ = tracing_subscriber::fmt::init();
|
||||||
@ -55,10 +80,38 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
drop(secret);
|
drop(secret);
|
||||||
tracing::info!("{:?}", secret_key.master());
|
tracing::debug!("{:?}", secret_key.master());
|
||||||
let redis_store = RedisSessionStore::new(redis_url.as_str()).await.unwrap();
|
let redis_store = RedisSessionStore::new(redis_url.as_str()).await.unwrap();
|
||||||
|
|
||||||
|
let search = {
|
||||||
|
let search_addr = crate::actors::search::SearchEngine::build()
|
||||||
|
.unwrap()
|
||||||
|
.start();
|
||||||
|
use crate::actors::search::*;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
let records = entities::prelude::Recipies::find()
|
||||||
|
.all(&db)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|recipe| RecipeRecord::from(recipe))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let title = records
|
||||||
|
.first()
|
||||||
|
.map(|rec| rec.title.clone())
|
||||||
|
.unwrap_or_else(|| "test".into());
|
||||||
|
let _ = search_addr.send(Refresh { records }).await;
|
||||||
|
let res = search_addr
|
||||||
|
.send(Find { query: title })
|
||||||
|
.await
|
||||||
|
.expect("Search can't fail")
|
||||||
|
.expect("Must find something");
|
||||||
|
tracing::debug!("Test search result: {res:?}");
|
||||||
|
Search(search_addr)
|
||||||
|
};
|
||||||
|
|
||||||
// Transform to data
|
// Transform to data
|
||||||
|
let search = Data::new(search);
|
||||||
let admins = Data::new(admins);
|
let admins = Data::new(admins);
|
||||||
let db = Data::new(db);
|
let db = Data::new(db);
|
||||||
let redis = Data::new(redis);
|
let redis = Data::new(redis);
|
||||||
@ -80,7 +133,9 @@ async fn main() {
|
|||||||
.app_data(admins.clone())
|
.app_data(admins.clone())
|
||||||
.app_data(db.clone())
|
.app_data(db.clone())
|
||||||
.app_data(redis.clone())
|
.app_data(redis.clone())
|
||||||
|
.app_data(search.clone())
|
||||||
.service(Files::new("/pages", "./pages"))
|
.service(Files::new("/pages", "./pages"))
|
||||||
|
.service(test_search)
|
||||||
.configure(routes::configure)
|
.configure(routes::configure)
|
||||||
})
|
})
|
||||||
.bind(&bind)
|
.bind(&bind)
|
||||||
|
Loading…
Reference in New Issue
Block a user