Search not working

This commit is contained in:
Adrian Woźniak 2024-10-29 17:19:10 +01:00
parent 6d487dd5c7
commit 427a6cb597
3 changed files with 145 additions and 9 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
/migration/target
node_modules
/indices

View File

@ -6,6 +6,26 @@ use tantivy::query::QueryParser;
use tantivy::{doc, Index, IndexWriter, ReloadPolicy, Searcher};
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 {
writer: IndexWriter,
schema: Schema,
@ -18,13 +38,17 @@ pub struct SearchEngine(Arc<Mutex<Inner>>);
impl SearchEngine {
pub fn build() -> Result<Self, tantivy::TantivyError> {
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();
schema_builder.add_u64_field("id", INDEXED);
schema_builder.add_text_field("title", TEXT);
schema_builder.add_text_field("summary", TEXT);
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 reader = index
@ -50,9 +74,7 @@ impl actix::Actor for SearchEngine {
#[derive(Debug, Message)]
#[rtype(result = "Result<u64,TantivyError>")]
pub struct CreateRecipe {
id: u64,
title: String,
summary: String,
pub record: RecipeRecord,
}
impl Handler<CreateRecipe> for SearchEngine {
@ -68,10 +90,11 @@ impl Handler<CreateRecipe> for SearchEngine {
let title = 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! {
id => msg.id,
title => msg.title,
summary => msg.summary,
summary => msg.summary.unwrap_or_default(),
})?;
shared.writer.commit()?;
@ -85,7 +108,7 @@ impl Handler<CreateRecipe> for SearchEngine {
#[derive(Debug, Message)]
#[rtype(result = "Result<Vec<u64>,TantivyError>")]
pub struct Find {
query: String,
pub query: String,
}
impl Handler<Find> for SearchEngine {
@ -102,19 +125,38 @@ impl Handler<Find> for SearchEngine {
let summary = shared.schema.get_field("summary").unwrap();
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 ids = rows
.into_iter()
.filter_map(|row| {
tracing::debug!("tantivy row: {row:?}");
let doc: Option<TantivyDocument> = shared.searcher.doc(row.1).ok();
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)
.filter_map(|id| id.as_u64())
.filter_map(|id| {
tracing::debug!("tantivy id: {id:?}");
id.as_u64()
})
.fold(agg, |mut agg, id| {
agg.push(id);
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),
)
}
}

View File

@ -1,3 +1,4 @@
use actix::Actor;
use actix_files::Files;
use actix_identity::IdentityMiddleware;
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
@ -15,6 +16,30 @@ pub mod types;
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]
async fn main() {
let _ = tracing_subscriber::fmt::init();
@ -55,10 +80,38 @@ async fn main() {
}
};
drop(secret);
tracing::info!("{:?}", secret_key.master());
tracing::debug!("{:?}", secret_key.master());
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
let search = Data::new(search);
let admins = Data::new(admins);
let db = Data::new(db);
let redis = Data::new(redis);
@ -80,7 +133,9 @@ async fn main() {
.app_data(admins.clone())
.app_data(db.clone())
.app_data(redis.clone())
.app_data(search.clone())
.service(Files::new("/pages", "./pages"))
.service(test_search)
.configure(routes::configure)
})
.bind(&bind)