implement html & opds search
All checks were successful
Build Multiarch Container Image / call-reusable-workflow (push) Successful in 27m24s
All checks were successful
Build Multiarch Container Image / call-reusable-workflow (push) Successful in 27m24s
This commit is contained in:
parent
ed8b69de13
commit
55d3364b0e
19 changed files with 302 additions and 8 deletions
|
@ -5,8 +5,11 @@ use std::path::Path;
|
|||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
use crate::data::{
|
||||
author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series,
|
||||
use crate::{
|
||||
data::{
|
||||
author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series,
|
||||
},
|
||||
search::{self, search},
|
||||
};
|
||||
|
||||
/// Top level calibre functions, bundling all sub functions in one place and providing secure access to
|
||||
|
@ -24,9 +27,18 @@ impl Calibre {
|
|||
let manager = SqliteConnectionManager::file(path);
|
||||
let pool = r2d2::Pool::new(manager)?;
|
||||
|
||||
search::attach(&pool)?;
|
||||
|
||||
Ok(Self { pool })
|
||||
}
|
||||
|
||||
/// Full text search with a query.
|
||||
///
|
||||
/// See https://www.sqlite.org/fts5.html#full_text_query_syntax for syntax.
|
||||
pub fn search(&self, query: &str) -> Result<Vec<Book>, DataStoreError> {
|
||||
search(query, &self.pool)
|
||||
}
|
||||
|
||||
/// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and
|
||||
/// ordering by `sort_order`.
|
||||
pub fn books(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
//! Read data from a calibre library, leveraging its SQLite metadata database.
|
||||
|
||||
pub mod calibre;
|
||||
pub mod search;
|
||||
|
||||
/// Data structs for the calibre database.
|
||||
pub mod data {
|
||||
pub mod author;
|
||||
|
|
71
calibre-db/src/search.rs
Normal file
71
calibre-db/src/search.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
//! Provide search funcitonality for calibre.
|
||||
//!
|
||||
//! Because the calibre database can not be disturbed (it is treated as read-only)
|
||||
//! it attaches an in-memory database and inserts the relevant data into a
|
||||
//! virtual table leveraging fts5 (https://www.sqlite.org/fts5.html). Full-text search is run on
|
||||
//! that virtual table.
|
||||
|
||||
use r2d2::{Pool, PooledConnection};
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::named_params;
|
||||
|
||||
use crate::data::{book::Book, error::DataStoreError};
|
||||
|
||||
/// A lot of joins but only run once at startup.
|
||||
const SEARCH_INIT_QUERY: &str = "INSERT INTO search.fts(book_id, data)
|
||||
SELECT b.id as book_id,
|
||||
b.title || ' ' ||
|
||||
a.name || ' ' ||
|
||||
c.text || ' ' ||
|
||||
GROUP_CONCAT(DISTINCT t.name) || ' ' ||
|
||||
GROUP_CONCAT(DISTINCT i.val) || ' ' ||
|
||||
GROUP_CONCAT(DISTINCT s.name) as data
|
||||
FROM main.books as b
|
||||
JOIN main.books_authors_link AS b2a ON b.id = b2a.book
|
||||
JOIN main.authors AS a ON b2a.author = a.id
|
||||
JOIN main.comments AS c ON c.book = b.id
|
||||
JOIN main.books_tags_link AS b2t ON b.id = b2t.book
|
||||
JOIN main.tags AS t ON b2t.tag = t.id
|
||||
JOIN main.identifiers AS i ON i.book = b.id
|
||||
JOIN main.books_series_link AS b2s ON b.id = b2s.book
|
||||
JOIN main.series AS s ON b2s.series = s.id";
|
||||
|
||||
/// Attach the fts in-memory database to the read-only calibre database.
|
||||
pub(crate) fn attach(pool: &Pool<SqliteConnectionManager>) -> Result<(), DataStoreError> {
|
||||
let conn = pool.get()?;
|
||||
|
||||
conn.execute("ATTACH DATABASE ':memory:' AS search", [])?;
|
||||
init(&conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialise the fts virtual table.
|
||||
fn init(conn: &PooledConnection<SqliteConnectionManager>) -> Result<(), DataStoreError> {
|
||||
conn.execute(
|
||||
"CREATE VIRTUAL TABLE search.fts USING fts5(book_id, data)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(SEARCH_INIT_QUERY, [])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a full-text search with the parameter `query`.
|
||||
pub(crate) fn search(
|
||||
query: &str,
|
||||
pool: &Pool<SqliteConnectionManager>,
|
||||
) -> Result<Vec<Book>, DataStoreError> {
|
||||
let conn = pool.get()?;
|
||||
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT book_id FROM search.fts WHERE data MATCH (:query) ORDER BY rank")?;
|
||||
let params = named_params! { ":query": query };
|
||||
let books = stmt
|
||||
.query_map(params, |r| -> Result<u64, rusqlite::Error> { r.get(0) })?
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|id| Book::scalar_book(&conn, id).ok())
|
||||
.collect();
|
||||
|
||||
Ok(books)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue