Skip to content
Permalink
Browse files

Merge pull request #132 from wezm/search

Add full text search for posts
  • Loading branch information
wezm committed Jan 13, 2020
2 parents 8219cbd + 212890f commit 779813c1ed983cd1b9976e5b9f73b6503a438b5a
@@ -7,6 +7,9 @@ class Posts::Create < BrowserAction
if post && save_categories(post) && save_tags(post, form)
flash.success = "The record has been saved"
response = redirect Show.with(post.id)
# NOTE: We call this manually here instead of using a callback because the callback
# fires too early, before the tags have been updated.
refresh_full_text_index
tx_result = true
else
flash.failure = "Unable to create Post"
@@ -48,4 +51,9 @@ class Posts::Create < BrowserAction
rescue Avram::InvalidOperationError
false
end

private def refresh_full_text_index
AppDatabase.run(&.exec "REFRESH MATERIALIZED VIEW search_view")
Lucky.logger.info("Refreshed search index")
end
end
@@ -10,6 +10,7 @@ class Posts::Update < BrowserAction
if post && save_categories(post, existing_category_ids) && save_tags(post, form, PostTagQuery.new.post_id(post.id).preload_tag)
flash.success = "The post has been updated"
response = redirect Show.with(post.id)
refresh_full_text_index
tx_result = true
else
flash.failure = "Unable to update Post"
@@ -67,4 +68,9 @@ class Posts::Update < BrowserAction
rescue Avram::InvalidOperationError
false
end

private def refresh_full_text_index
AppDatabase.run(&.exec "REFRESH MATERIALIZED VIEW search_view")
Lucky.logger.info("Refreshed search index")
end
end
@@ -0,0 +1,15 @@
class Search::Show < BrowserAction
include Auth::AllowGuests

param q : String = ""
param page : Int32 = 1 # Trying to make this UInt16 or UInt32 gives Error: undefined constant UInt32::Lucky

get "/search" do
if q.blank?
flash.failure = "You need to specify what to search for"
html Search::ShowPage, query: q, results: SearchResults.none
else
html Search::ShowPage, query: q, results: PostQuery.search(q, Page.new(page))
end
end
end
@@ -0,0 +1,61 @@
class Posts::Pagination < BaseComponent
needs query : String
needs page : Page
needs total : UInt32
needs per_page : UInt16

@total_pages : UInt16?

def render
div class: "pagination" do
first_page if total_pages > 2
prev_page if total_pages > 1
span class: "pagination-link" do
raw "Page #{@page}&nbsp;of&nbsp;#{total_pages}"
end
next_page if total_pages > 1
last_page if total_pages > 2
end
end

private def prev_page
if @page.first?
span "← Previous", class: "pagination-link"
else
page_link "← Previous", @page.pred
end
end

private def next_page
if @page.to_u32 >= total_pages
span "Next →", class: "pagination-link"
else
page_link "Next →", @page.succ
end
end

private def first_page
if @page.first?
span "⇤ First", class: "pagination-link pagination-link-bounds"
else
page_link "⇤ First", 1, "pagination-link-bounds"
end
end

private def last_page
if @page.to_u32 == total_pages
span "Last ⇥", class: "pagination-link pagination-link-bounds"
else
page_link "Last ⇥", total_pages, "pagination-link-bounds"
end
end

private def page_link(text, page : UInt16, klass : String = "")
# FIXME: Don't hardcode the route helper
link text, to: Search::Show.with(q: @query, page: page.to_i), class: "pagination-link #{klass}"
end

private def total_pages : UInt16
@total_pages ||= (@total.to_f / @per_page.to_f).ceil.to_u16
end
end
@@ -2,20 +2,28 @@ class Posts::Summary < BaseComponent
needs post : Post
needs current_user : User?
needs show_categories : Bool = false
needs highlight : String? = nil

private SUBSTITUTIONS = {
'\u0012' => "<mark>",
'\u0014' => "</mark>",
}

def render
article do
a @post.title, href: @post.url
text " by #{@post.author}"
if @show_categories
text " in "
@post.post_categories.each_with_index do |cat, index|
text ", " if index != 0
link cat.name, to: Categories::Show.with(cat.slug)
div class: "article-meta" do
a @post.title, href: @post.url
text " by #{@post.author}"
if @show_categories
text " in "
@post.post_categories.each_with_index do |cat, index|
text ", " if index != 0
link cat.name, to: Categories::Show.with(cat.slug)
end
end
end
tag "blockquote" do
simple_format(HTML.escape(@post.summary))
simple_format(safe_summary)
end

@post.tags.each do |tag|
@@ -25,4 +33,9 @@ class Posts::Summary < BaseComponent
mount Posts::ActionBar.new(@post, @current_user)
end
end

def safe_summary : String
summary = @highlight || @post.summary
HTML.escape(summary).gsub(SUBSTITUTIONS)
end
end
@@ -0,0 +1,11 @@
class Search::Form < BaseComponent
needs query : String = ""

def render
form action: "/search", class: "search-form", method: "get" do
input aria_label: "Search Read Rust", autocapitalize: "off", autocomplete: "off", id: "q", maxlength: "255", name: "q", placeholder: "Search", title: "Search Read Rust", type: "search", value: @query
text " "
input type: "submit", value: "Search"
end
end
end
@@ -1,15 +1,20 @@
class Shared::Header < BaseComponent
needs current_user : User?
needs query : String = ""

def render
current_user = @current_user

header do
link to: Home::Index do
text "Read "
img alt: "", class: "logo", src: asset("images/logo.svg")
text " Rust"
div class: "logo-search-header #{@query.blank? ? "" : "logo-search-header-with-query"}" do
link to: Home::Index do
img alt: "", class: "logo", src: asset("images/logo.svg")
text "Read Rust"
end

mount Search::Form.new(@query)
end

nav do
div class: "list-inline" do
div do
@@ -36,29 +36,40 @@ h1,h2,h3 {
color: #333;
}
footer {
color: #ccc;
text-align: center;
border-top: 1px solid #fbd492;
border-bottom: 1px solid #fbd492;
background-color: #fff9f2;
border-top: 2px solid #333;
border-bottom: 1px solid #111;
background-color: #444;
padding: 1em;
margin-top: 4em;
}
footer a {
color: #ccc;
}
header {
font-size: 1.5em;
font-weight: 500;
color: black;
padding: 1em 1em 2em;
text-align: center;
background-color: #fff9f2;
border-bottom: 1px solid #fde8c4;
margin-bottom: 4em;
}
header a {
color: currentColor;
text-decoration: none;
}
header .search-form {
text-align: right;
}
h1 a {
color: currentColor;
text-decoration: none;
border-bottom: 3px solid currentColor;
}
nav {
text-align: center;
}
nav a {
font-size: 18px;
text-transform: uppercase;
@@ -71,11 +82,18 @@ nav a:hover, nav a:focus {
article {
margin-top: 2em;
}
.article-meta {
font-size: 1.1em;
}
blockquote {
margin-left: 0;
padding-left: 1em;
border-left: 3px solid #fde8c4;
}
mark {
background-color: #fffdfd;
font-weight: bold;
}
:target {
background-color: #fff9f2;
padding: 0.25em 0.5em;
@@ -94,11 +112,29 @@ blockquote {
.list-inline .support {
width: 6em;
}
.logo-search-header {
padding-top: 1.5em;
margin: 0 auto;
max-width: 750px;
display: flex;
justify-content: space-between;

& > * {
flex: 1 0 auto;
}
}
.logo-search-header-with-query {
text-align: center;

.search-form {
display: none;
}
}
.logo {
vertical-align: top;
width: 50px;
height: 40px;
margin: 0 0.5em;
margin: 0 0.5em 0.5em 0;
}
.heart {
position: relative;
@@ -210,7 +246,6 @@ blockquote {
display: flex;
flex-direction: column;
justify-content: space-between;
/* border: 1px solid #ffebd4; */
border-radius: 3px;
margin: 1.5em 0;
}
@@ -248,10 +283,19 @@ blockquote {
clip: rect(1px, 1px, 1px, 1px);
}

#search {
margin-bottom: 2em;
.pagination {
text-align: center;
margin-top: 2em;
font-size: 1.1em;
}
.pagination-link {
padding-right: 1em;
}
#search input[type="text"] {
a.pagination-link {
color: blue;
}

.search-form input[type="text"] {
width: 20em;
}
.socials {
@@ -264,21 +308,47 @@ blockquote {
}

@media (max-width: 768px) {
header {
padding-bottom: 1em;
padding-left: 10px;
padding-bottom: 10px;
}
.main {
margin-top: 0;
}
.logo {
padding-left: 0.5em;
}
.search-form {
padding-right: 0.5em;

input {
font-size: 16px;
}
}
.feedicon {
margin-top: 1em;
}
}
@media (max-width: 570px) {
header {
text-align: center;
margin-bottom: 1em;

.logo-search-header {
justify-content: center;
flex-wrap: wrap;
padding-bottom: 1em;
padding-left: 10px;
padding-bottom: 10px;
}
.search-form {
text-align: center;
margin-top: 0.5em;
}
}
}
@media (max-width: 460px) {
.list-inline {
flex-wrap: wrap;
}
.pagination-link-bounds {
display: none;
}
}

@@ -0,0 +1,18 @@
# A type representing a page number
#
# Will always be in the range 1..=UInt16::MAX
struct Page
delegate to_i, to_u16, to_u32, to_s, succ, pred, to: @page

def initialize(page)
if page < 1 || page > UInt16::MAX
@page = 1_u16
else
@page = page.to_u16
end
end

def first?
@page == 1_u16
end
end

0 comments on commit 779813c

Please sign in to comment.
You can’t perform that action at this time.