about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@gmail.com>2018-04-14T18·28+0200
committerVincent Ambo <github@tazj.in>2018-04-14T20·21+0200
commit31b0a550f2b96a1de5de65308420788c9a6aa5df (patch)
tree27f6396644f753aef6a8ebf48ba0bd6438e71029
parent2d8db520107c25c5bc7d1ba861047fa9b7eaea5f (diff)
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.
-rw-r--r--src/db.rs41
-rw-r--r--src/models.rs21
2 files changed, 61 insertions, 1 deletions
diff --git a/src/db.rs b/src/db.rs
index 5a66fbb0fc74..416e3fdd0f52 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<CreatePost> 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<Vec<SearchResult>>;
+}
+
+/// 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<SearchPosts> for DbExecutor {
+    type Result = <SearchPosts as Message>::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::<Text, _>(msg.query)
+            .get_results::<SearchResult>(&conn)?;
+
+        Ok(search_results)
+    }
+}
diff --git a/src/models.rs b/src/models.rs
index 9d3405e1540f..927a78513669 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,
+}