little-hesinde/calibre-db/src/search.rs
Sebastian Hugentobler 55d3364b0e
All checks were successful
Build Multiarch Container Image / call-reusable-workflow (push) Successful in 27m24s
implement html & opds search
2024-06-26 13:53:00 +02:00

72 lines
2.4 KiB
Rust

//! 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)
}