From 31b0a550f2b96a1de5de65308420788c9a6aa5df Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 14 Apr 2018 20:28:30 +0200 Subject: [PATCH] feat(db): Implement handling of 'SearchPosts' message Adds support for executing full-text search across a forum instance by sending the `SearchPosts` message with a search query to the DB actor. The struct used for results is mapped manually to the expected query result as the query is embedded via raw SQL. --- src/db.rs | 41 ++++++++++++++++++++++++++++++++++++++++- src/models.rs | 21 +++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/db.rs b/src/db.rs index 5a66fbb0f..416e3fdd0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -17,7 +17,8 @@ //! This module implements the database connection actor. use actix::prelude::*; -use diesel; +use diesel::{self, sql_query}; +use diesel::sql_types::Text; use diesel::prelude::*; use diesel::r2d2::{Pool, ConnectionManager}; use models::*; @@ -138,3 +139,41 @@ impl Handler for DbExecutor { .get_result(&conn)?) } } + +/// Message used to search for posts +#[derive(Deserialize)] +pub struct SearchPosts { pub query: String } + +impl Message for SearchPosts { + type Result = Result>; +} + +/// Raw PostgreSQL query used to perform full-text search on posts +/// with a supplied phrase. For now, the query language is hardcoded +/// to English and only "plain" queries (i.e. no searches for exact +/// matches or more advanced query syntax) are supported. +const SEARCH_QUERY: &'static str = r#" +WITH search_query (query) AS (VALUES (plainto_tsquery('english', $1))) +SELECT post_id, + thread_id, + author, + title, + ts_headline('english', body, query) AS headline + FROM search_index, search_query + WHERE document @@ query + ORDER BY ts_rank(document, query) DESC +"#; + +impl Handler for DbExecutor { + type Result = ::Result; + + fn handle(&mut self, msg: SearchPosts, _: &mut Self::Context) -> Self::Result { + let conn = self.0.get()?; + + let search_results = sql_query(SEARCH_QUERY) + .bind::(msg.query) + .get_results::(&conn)?; + + Ok(search_results) + } +} diff --git a/src/models.rs b/src/models.rs index 9d3405e15..927a78513 100644 --- a/src/models.rs +++ b/src/models.rs @@ -16,6 +16,7 @@ use chrono::prelude::{DateTime, Utc}; use schema::{threads, posts}; +use diesel::sql_types::{Text, Integer}; #[derive(Identifiable, Queryable, Serialize)] pub struct Thread { @@ -69,3 +70,23 @@ pub struct NewPost { pub author_name: String, pub author_email: String, } + +/// This struct models the response of a full-text search query. It +/// does not use a table/schema definition struct like the other +/// tables, as no table of this type actually exists. +#[derive(QueryableByName, Debug)] +pub struct SearchResult { + #[sql_type = "Integer"] + pub post_id: i32, + #[sql_type = "Integer"] + pub thread_id: i32, + #[sql_type = "Text"] + pub author: String, + #[sql_type = "Text"] + pub title: String, + + /// Headline represents the result of Postgres' ts_headline() + /// function, which highlights search terms in the search results. + #[sql_type = "Text"] + pub headline: String, +}