~vpzom/lotide

d19fd96718bb0474d0310b8bed40a28466f74ac9 — Colin Reeder 10 months ago 3419324 + 28837ef
Merge remote-tracking branch 'origin/post-search'
A migrations/20201026222917_post-fts/down.sql => migrations/20201026222917_post-fts/down.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	DROP INDEX post_fts;
COMMIT;

A migrations/20201026222917_post-fts/up.sql => migrations/20201026222917_post-fts/up.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	CREATE INDEX post_fts ON post USING gin(to_tsvector('english', title || ' ' || COALESCE(content_text, content_markdown, content_html, '')));
COMMIT;

M openapi/openapi.json => openapi/openapi.json +17 -0
@@ 1078,6 1078,23 @@
						"in": "query",
						"required": false,
						"schema": {"type": "boolean"}
					},
					{
						"name": "sort",
						"in": "query",
						"required": false,
						"schema": {
							"oneOf": [
								{"$ref": "#/components/schemas/SortType"},
								{"type": "string", "enum": ["relevant"]}
							]
						}
					},
					{
						"name": "search",
						"in": "query",
						"required": false,
						"schema": {"type": "string"}
					}
				],
				"responses": {

M res/lang/en.ftl => res/lang/en.ftl +1 -0
@@ 28,6 28,7 @@ post_needs_content = Post must contain one of href, content_text, or content_mar
post_not_in_community = That post is not in this community
post_not_yours = That's not your post
root = lotide is running. Note that lotide itself does not include a frontend, and you'll need to install one separately.
sort_relevant_not_search = Sorting by relevance is only allowed when searching
user_email_invalid = Specified email address is invalid
user_name_disallowed_chars = Username contains disallowed characters
user_no_avatar = That user does not have an avatar

M src/routes/api/posts.rs => src/routes/api/posts.rs +69 -11
@@ 1,11 1,12 @@
use super::{
    JustURL, MaybeIncludeYour, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo,
    JustURL, RespAvatarInfo, RespMinimalAuthorInfo, RespMinimalCommentInfo,
    RespMinimalCommunityInfo, RespPostCommentInfo, RespPostListPost,
};
use crate::{CommentLocalID, CommunityLocalID, PostLocalID, UserLocalID};
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt::Write;
use std::sync::Arc;

async fn get_post_comments<'a>(


@@ 110,8 111,40 @@ async fn route_unstable_posts_list(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let query: MaybeIncludeYour = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?;
    #[derive(Deserialize)]
    #[serde(rename_all = "snake_case")]
    enum PostsListExtraSortType {
        Relevant,
    }

    #[derive(Deserialize)]
    #[serde(untagged)]
    enum PostsListSortType {
        Normal(super::SortType),
        Extra(PostsListExtraSortType),
    }

    impl Default for PostsListSortType {
        fn default() -> Self {
            Self::Normal(super::SortType::Hot)
        }
    }

    #[derive(Deserialize)]
    struct PostsListQuery<'a> {
        #[serde(default)]
        search: Option<Cow<'a, str>>,

        #[serde(default)]
        include_your: bool,

        #[serde(default)]
        sort: PostsListSortType,
    }

    let query: PostsListQuery = serde_urlencoded::from_str(req.uri().query().unwrap_or(""))?;

    let lang = crate::get_lang_for_req(&req);
    let db = ctx.db_pool.get().await?;

    let include_your_for = if query.include_your {


@@ 121,23 154,48 @@ async fn route_unstable_posts_list(
        None
    };

    let mut search_value_idx = None;

    let limit: i64 = 30;

    let mut values: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> = vec![&limit];

    let include_your_idx = match &include_your_for {
        None => None,
        Some(user) => {
            values.push(user);
            Some(values.len())
        }
    let include_your_idx = if let Some(user) = &include_your_for {
        values.push(user);
        Some(values.len())
    } else {
        None
    };

    let sql: &str = &format!(
        "SELECT {} FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND deleted=FALSE ORDER BY hot_rank((SELECT COUNT(*) FROM post_like WHERE post = post.id AND person != post.author), post.created) DESC LIMIT $1",
        super::common_posts_list_query(include_your_idx),
    let mut sql = format!(
        "SELECT {}",
        super::common_posts_list_query(include_your_idx)
    );

    sql.push_str( " FROM community, post LEFT OUTER JOIN person ON (person.id = post.author) WHERE post.community = community.id AND deleted=FALSE");
    if let Some(search) = &query.search {
        values.push(search);
        search_value_idx = Some(values.len());
        write!(sql, " AND to_tsvector('english', title || ' ' || COALESCE(content_text, content_markdown, content_html, '')) @@ plainto_tsquery('english', ${})", values.len()).unwrap();
    }
    sql.push_str(" ORDER BY ");
    match query.sort {
        PostsListSortType::Normal(ty) => sql.push_str(ty.post_sort_sql()),
        PostsListSortType::Extra(PostsListExtraSortType::Relevant) => {
            if let Some(search_value_idx) = search_value_idx {
                write!(sql, "ts_rank_cd(to_tsvector('english', title || ' ' || COALESCE(content_text, content_markdown, content_html, '')), plainto_tsquery('english', ${}))", search_value_idx).unwrap();
            } else {
                return Err(crate::Error::UserError(crate::simple_response(
                    hyper::StatusCode::BAD_REQUEST,
                    lang.tr("sort_relevant_not_search", None).into_owned(),
                )));
            }
        }
    }
    sql.push_str(" LIMIT $1");

    let sql: &str = &sql;

    let stream = crate::query_stream(&db, sql, &values).await?;

    let posts = super::handle_common_posts_list(stream, &ctx, include_your_for.is_some()).await?;