Experimentally transform markdown for better presentation in full text search results

For issue #37
This commit is contained in:
Magnus Hoff 2018-01-19 16:49:15 +01:00
parent e499a095c7
commit b1e598cb17
10 changed files with 80 additions and 12 deletions

View file

@ -37,7 +37,7 @@ function debouncer(interval, callback) {
const query = input.value; const query = input.value;
fetch( fetch(
"_search?snippet_size=4&limit=4&q=" + encodeURIComponent(query), "_search?snippet_size=10&limit=4&q=" + encodeURIComponent(query),
{ {
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@ -56,7 +56,7 @@ function debouncer(interval, callback) {
item.querySelector('.link').href = hit.slug || "."; item.querySelector('.link').href = hit.slug || ".";
item.querySelector('.link').setAttribute("data-focusindex", index + 1); item.querySelector('.link').setAttribute("data-focusindex", index + 1);
item.querySelector('.title').textContent = hit.title; item.querySelector('.title').textContent = hit.title;
item.querySelector('.snippet').textContent = hit.snippet; item.querySelector('.snippet').innerHTML = hit.snippet;
results.appendChild(item); results.appendChild(item);
}) })

View file

@ -305,9 +305,6 @@ article ul.search-results {
.search-result .title { .search-result .title {
font-weight: bold; font-weight: bold;
} }
.snippet {
white-space: pre-line;
}
.search-result p { .search-result p {
margin: 0; margin: 0;
} }

View file

@ -11,6 +11,11 @@ use std::io::prelude::*;
use std::path::Path; use std::path::Path;
use walkdir::WalkDir; use walkdir::WalkDir;
use std::ffi::CString;
fn markdown_to_fts(_: &::diesel::sqlite::Context) -> CString {
panic!("Should never be called when running migrations on build.db")
}
fn main() { fn main() {
let out_dir = env::var("OUT_DIR").expect("cargo must set OUT_DIR"); let out_dir = env::var("OUT_DIR").expect("cargo must set OUT_DIR");
let db_path = Path::new(&out_dir).join("build.db"); let db_path = Path::new(&out_dir).join("build.db");
@ -18,14 +23,21 @@ fn main() {
let _ignore_failure = std::fs::remove_file(db_path); let _ignore_failure = std::fs::remove_file(db_path);
let connection = SqliteConnection::establish(db_path) let mut connection = SqliteConnection::establish(db_path)
.expect(&format!("Error esablishing a database connection to {}", db_path)); .expect(&format!("Error esablishing a database connection to {}", db_path));
// Integer is a dummy placeholder. Compiling fails when passing (). // Integer is a dummy placeholder. Compiling fails when passing ().
diesel::expression::sql_literal::sql::<(diesel::types::Integer)>("PRAGMA foreign_keys = ON") diesel::expression::sql_literal::sql::<(diesel::sql_types::Integer)>("PRAGMA foreign_keys = ON")
.execute(&connection) .execute(&connection)
.expect("Should be able to enable foreign keys"); .expect("Should be able to enable foreign keys");
connection.create_scalar_function(
"markdown_to_fts",
1,
true,
markdown_to_fts,
).unwrap();
diesel_migrations::run_pending_migrations(&connection).unwrap(); diesel_migrations::run_pending_migrations(&connection).unwrap();
let infer_schema_path = Path::new(&out_dir).join("infer_schema.rs"); let infer_schema_path = Path::new(&out_dir).join("infer_schema.rs");

View file

@ -0,0 +1,24 @@
DROP TRIGGER article_revisions_ai;
DROP TRIGGER article_revisions_ad;
DROP TRIGGER article_revisions_au_disable;
DROP TRIGGER article_revisions_au_enable;
CREATE TRIGGER article_revisions_ai AFTER INSERT ON article_revisions WHEN new.latest = 1 BEGIN
DELETE FROM article_search WHERE rowid = new.article_id;
INSERT INTO article_search(rowid, title, body, slug) VALUES (new.article_id, new.title, markdown_to_fts(new.body), new.slug);
END;
CREATE TRIGGER article_revisions_ad AFTER DELETE ON article_revisions WHEN old.latest = 1 BEGIN
DELETE FROM article_search WHERE rowid = old.article_id;
END;
-- Index unique_latest_revision_per_article_id makes sure the following is sufficient:
CREATE TRIGGER article_revisions_au_disable AFTER UPDATE ON article_revisions WHEN old.latest = 1 AND new.latest = 0 BEGIN
DELETE FROM article_search WHERE rowid = old.article_id;
END;
CREATE TRIGGER article_revisions_au_enable AFTER UPDATE ON article_revisions WHEN old.latest = 0 AND new.latest = 1 BEGIN
INSERT INTO article_search(rowid, title, body, slug) VALUES (new.article_id, new.title, markdown_to_fts(new.body), new.slug);
END;
DELETE FROM article_search;
INSERT INTO article_search(title, body, slug)
SELECT title, markdown_to_fts(body), slug FROM article_revisions WHERE latest = 1;

View file

@ -9,12 +9,27 @@ embed_migrations!();
#[derive(Debug)] #[derive(Debug)]
struct SqliteInitializer; struct SqliteInitializer;
use std::ffi::CString;
fn markdown_to_fts(ctx: &::diesel::sqlite::Context) -> CString {
use rendering;
CString::new(rendering::render_markdown_for_fts(&ctx.get::<String>(0))).unwrap()
}
impl CustomizeConnection<SqliteConnection, r2d2_diesel::Error> for SqliteInitializer { impl CustomizeConnection<SqliteConnection, r2d2_diesel::Error> for SqliteInitializer {
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2_diesel::Error> { fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2_diesel::Error> {
sql::<(Integer)>("PRAGMA foreign_keys = ON") sql::<(Integer)>("PRAGMA foreign_keys = ON")
.execute(conn) .execute(conn)
.and(Ok(())) .map_err(|x| r2d2_diesel::Error::QueryError(x))?;
.map_err(|x| r2d2_diesel::Error::QueryError(x))
conn.create_scalar_function(
"markdown_to_fts",
1,
true,
markdown_to_fts,
).map_err(|x| r2d2_diesel::Error::QueryError(x))?;
Ok(())
} }
} }

View file

@ -1,4 +1,5 @@
use pulldown_cmark::{Parser, html, OPTION_ENABLE_TABLES, OPTION_DISABLE_HTML}; use pulldown_cmark::{Parser, html, OPTION_ENABLE_TABLES, OPTION_DISABLE_HTML};
use pulldown_cmark::Event::Text;
pub fn render_markdown(src: &str) -> String { pub fn render_markdown(src: &str) -> String {
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML; let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
@ -7,3 +8,22 @@ pub fn render_markdown(src: &str) -> String {
html::push_html(&mut buf, p); html::push_html(&mut buf, p);
buf buf
} }
pub fn render_markdown_for_fts(src: &str) -> String {
let opts = OPTION_ENABLE_TABLES | OPTION_DISABLE_HTML;
let p = Parser::new_ext(src, opts);
let mut buf = String::new();
for event in p {
match event {
Text(text) => buf.push_str(&text),
_ => buf.push_str(" "),
}
}
buf.replace('&', "");
buf.replace('<', "");
buf.replace('>', "");
buf
}

View file

@ -12,7 +12,7 @@ use state::State;
use web::{Resource, ResponseFuture}; use web::{Resource, ResponseFuture};
const DEFAULT_LIMIT: u32 = 10; const DEFAULT_LIMIT: u32 = 10;
const DEFAULT_SNIPPET_SIZE: u32 = 8; const DEFAULT_SNIPPET_SIZE: u32 = 25;
type BoxResource = Box<Resource + Sync + Send>; type BoxResource = Box<Resource + Sync + Send>;

View file

@ -386,7 +386,7 @@ impl<'a> SyncState<'a> {
Ok( Ok(
sql_query( sql_query(
"SELECT title, snippet(article_search, 1, '', '', '\u{2026}', ?) AS snippet, slug \ "SELECT title, snippet(article_search, 1, '<em>', '</em>', '\u{2026}', ?) AS snippet, slug \
FROM article_search \ FROM article_search \
WHERE article_search MATCH ? \ WHERE article_search MATCH ? \
ORDER BY rank \ ORDER BY rank \

View file

@ -13,7 +13,7 @@
<ul class="search-results default-keyboard-focus-control"> <ul class="search-results default-keyboard-focus-control">
{{#hits}} {{#hits}}
<li class="search-result"><a data-focusindex="{{.0}}" class="link" href="{{.1.link()}}"><p class="title">{{.1.title}}</p><p class="snippet">{{.1.snippet}}</p></a></li> <li class="search-result"><a data-focusindex="{{.0}}" class="link" href="{{.1.link()}}"><p class="title">{{.1.title}}</p><p class="snippet">{{{.1.snippet}}}</p></a></li>
{{/hits}} {{/hits}}
</ul> </ul>
{{/hits}} {{/hits}}