Enable fuzzy searching

* calibre-cli.el: (calibre-cli--search-operation): Add a new argument
  docstring, specifying the docstring of the generated function.
  Add a new argument fuzzy-match to the generated function.
  Condition the search query of the generated function on the value of
  fuzzy-match.
* calibre-db.el (calibre-db--get-title-books):
  (calibre-db--query): New macro to generate SQL queries with
  potentially fuzzy matching and handling their output.
  (calibre-db--search-function): New macro to generate search functions.
  (calibre-db--get-title-books):
  (calibre-db--get-author-books):
  (calibre-db--get-tag-books):
  (calibre-db--get-publisher-books):
  (calibre-db--get-series-books):
  (calibre-db--get-format-books):

  Define using calibre-db--search-function.

* calibre-search.el (calibre-search--fuzzy-search-p): New predicate
  function.
  (calibre-search--make-filter):
  (calibre-search--make-composite-filter-component):

  New functions.

  (calibre-library--search-function):
  (calibre-search): Add option for fuzzy searching.
  (calibre-search--composition-function): Handle transient arguments
  consistently with calibre-library--search-function.
* doc/calibre.texi (Virtual Libraries): Document fuzzy filters.
* etc/NEWS: Mention the new fuzzy search functionality.
This commit is contained in:
Kjartan Oli Agustsson 2024-01-16 16:12:05 +00:00
parent 6446221d20
commit d8c85ae51b
Signed by: kjartanoli
GPG Key ID: D7572FE3605EE6B0
6 changed files with 135 additions and 62 deletions

View File

@ -1,6 +1,6 @@
;;; calibre-cli.el --- Fallback CLI interface when SQLite is not available -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Copyright (C) 2023,2024 Free Software Foundation, Inc.
;; This file is part of calibre.el.
@ -97,9 +97,12 @@ AUTHORS should be a comma separated string."
"Return the File Name of the book whose files are FILES."
(file-name-base (car files)))
(defmacro calibre-cli--search-operation (field)
"Create a function to search for books matching FIELD."
`(defun ,(intern (format "calibre-cli--get-%s-books" field)) (,field)
(defmacro calibre-cli--search-operation (field docstring)
"Create a function to search for books matching FIELD.
DOCSTRING is the docstring of the created function."
`(defun ,(intern (format "calibre-cli--get-%s-books" field)) (,field &optional fuzzy-match)
,docstring
(with-temp-buffer
(call-process calibre-calibredb-executable
nil
@ -108,18 +111,18 @@ AUTHORS should be a comma separated string."
"search"
"--with-library"
(calibre--library)
(format ,(format "%s:=%%s" field) ,field))
(format ,(format "%s%%s%%s" field ) (if fuzzy-match ":" ":=") ,field))
(mapcar #'cl-parse-integer
(string-split
(buffer-substring-no-properties (point-min) (point-max))
",")))))
(calibre-cli--search-operation title)
(calibre-cli--search-operation series)
(calibre-cli--search-operation publisher)
(calibre-cli--search-operation format)
(calibre-cli--search-operation tag)
(calibre-cli--search-operation author)
(calibre-cli--search-operation title "Return the id's of books whose title is TITLE.")
(calibre-cli--search-operation series "Return the id's of books that are part of SERIES.")
(calibre-cli--search-operation publisher "Return the id's of books published by PUBLISHER.")
(calibre-cli--search-operation format "Return the id's of books available in FORMAT.")
(calibre-cli--search-operation tag "Return the id's of books tagged with TAG.")
(calibre-cli--search-operation author "Return the id's of books written by AUTHOR.")
(defun calibre-cli--get-titles ()
"Return a list of the titles in the active library."

View File

@ -1,6 +1,6 @@
;;; calibre-core.el --- Abstract interface for the Calibre Library -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Copyright (C) 2023,2024 Free Software Foundation, Inc.
;; This file is part of calibre.el.
@ -219,14 +219,15 @@ BOOK is a `calibre-book'."
(if (eq op '+)
'()
(calibre--books))))
(seq-let (_ field value) filter
(cl-case field
(title (calibre-core--interface get-title-books value))
(author (calibre-core--interface get-author-books value))
(tag (calibre-core--interface get-tag-books value))
(publisher (calibre-core--interface get-publisher-books value))
(series (calibre-core--interface get-series-books value))
(format (calibre-core--interface get-format-books value))))))
(seq-let (_ field value &rest params) filter
(let ((fuzzy-match (seq-contains-p params '~)))
(cl-case field
(title (calibre-core--interface get-title-books value fuzzy-match))
(author (calibre-core--interface get-author-books value fuzzy-match))
(tag (calibre-core--interface get-tag-books value fuzzy-match))
(publisher (calibre-core--interface get-publisher-books value fuzzy-match))
(series (calibre-core--interface get-series-books value fuzzy-match))
(format (calibre-core--interface get-format-books value fuzzy-match)))))))
(defun calibre-library--filter (filters books)
"Return those books in BOOKS that match FILTERS.

View File

@ -1,6 +1,6 @@
;;; calibre-db.el --- Interact with the Calibre database -*- lexical-binding:t -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Copyright (C) 2023,2024 Free Software Foundation, Inc.
;; Author: Kjartan Oli Agustsson <kjartanoli@disroot.org>
;; Maintainer: Kjartan Oli Agustsson <kjartanoli@disroot.org>
@ -151,50 +151,70 @@ FROM books
LEFT JOIN books_series_link sl ON books.id = sl.book
LEFT JOIN series ON sl.series = series.id;"))))
(defun calibre-db--get-title-books (title)
"Return the id's of books whose title is TITLE."
(flatten-list (sqlite-select (calibre--db)
"SELECT id FROM books WHERE title = ?"
`[,title])))
(defmacro calibre-db--query (query arg fuzzy)
"Run QUERY on the Calibre database.
(defun calibre-db--get-author-books (author)
ARG is a value to passed to a WHERE clause in QUERY. FUZZY
determines whether the WHERE clause matches using LIKE or exact
matching. If FUZZY is non-nil LIKE will be used. QUERY must contain exactly one %s where the matching against ARG should be substituted.
This means QUERY should probably end:
WHERE column %s
with column being the name of some column."
`(flatten-list (sqlite-select (calibre--db)
(format ,query (if ,fuzzy "LIKE ?" "= ?"))
(if fuzzy-match
(vector (format "%%%s%%" ,arg))
(vector ,arg)))))
(defmacro calibre-db--search-function (field docstring query)
"Create a search function for FIELD with DOCSTRING as docstring.
QUERY is the SQL query used to perform the search, suitable as an
argument to `calibre-db--query'."
(declare (indent 1))
`(defun ,(intern (format "calibre-db--get-%s-books" field))
(,field &optional fuzzy-match)
,docstring
(calibre-db--query ,query ,field fuzzy-match)))
(calibre-db--search-function title
"Return the id's of books whose title is TITLE."
"SELECT id FROM books WHERE title %s")
(calibre-db--search-function author
"Return the id's of books written by AUTHOR."
(flatten-list (sqlite-select (calibre--db)
"SELECT book
"SELECT book
FROM books_authors_link al
LEFT JOIN authors a ON al.author = a.id
WHERE a.name = ?" `[,author])))
WHERE a.name %s")
(defun calibre-db--get-tag-books (tag)
(calibre-db--search-function tag
"Return the id's of books tagged with TAG."
(flatten-list (sqlite-select (calibre--db)
"SELECT book
"SELECT book
FROM books_tags_link tl
LEFT JOIN tags t ON tl.tag = t.id
WHERE t.name = ?" `[,tag])))
WHERE t.name %s")
(defun calibre-db--get-publisher-books (publisher)
(calibre-db--search-function publisher
"Return the id's of books published by PUBLISHER."
(flatten-list (sqlite-select (calibre--db)
"SELECT book
"SELECT book
FROM books_publishers_link pl
LEFT JOIN publishers p ON pl.publisher = p.id
WHERE p.name = ?" `[,publisher])))
WHERE p.name %s")
(defun calibre-db--get-series-books (series)
(calibre-db--search-function series
"Return the id's of books that are part of SERIES."
(flatten-list (sqlite-select (calibre--db)
"SELECT book
"SELECT book
FROM books_series_link sl
LEFT JOIN series s ON sl.series = s.id
WHERE s.name = ?" `[,series])))
WHERE s.name %s")
(defun calibre-db--get-format-books (format)
(calibre-db--search-function format
"Return the id's of books available in FORMAT."
(flatten-list (sqlite-select (calibre--db)
"SELECT book
"SELECT book
FROM data
WHERE format = ?" `[,format])))
WHERE format %s")
(provide 'calibre-db)
;;; calibre-db.el ends here

View File

@ -1,5 +1,5 @@
;;; calibre-search.el --- Filter books based on search criteria. -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Copyright (C) 2023,2024 Free Software Foundation, Inc.
;; This file is part of calibre.el.
@ -59,16 +59,32 @@ ARGS is the argument list of a transient command."
'-
'+))
(defun calibre-search--fuzzy-search-p (args)
"Return t if the filter operation should use fuzzy matching.
ARGS is the argument list of a transient command."
(seq-contains-p args "--fuzzy"))
(defun calibre-search--make-filter (op field val fuzzy)
"Create a search filter.
OP is a symbol, either + or - for inclusive or exclusive. FIELD
is a symbol identifying the field to search. VAL is the value to
search for. If FUZZY is non-nil create a fuzzy filter."
(if fuzzy
`[,op ,field ,val ~]
`[,op ,field ,val]))
(defmacro calibre-library--search-function (field)
"Create a function adding a filter for FIELD."
`(defun ,(intern (format "calibre-library-search-%s" field)) (val &optional args)
(interactive (list (,(intern (format "calibre-search-chose-%s" field)))
(transient-args 'calibre-search)))
(setf calibre-library--filters (cons
(vector (calibre-search--operation args)
(quote ,(intern field))
val)
calibre-library--filters))
(push (calibre-search--make-filter
(calibre-search--operation args)
(quote ,(intern field))
val
(calibre-search--fuzzy-search-p args))
calibre-library--filters)
(calibre-library--refresh)))
(calibre-library--search-function "title")
@ -89,7 +105,8 @@ ARGS is the argument list of a transient command."
"Filter the library view."
:transient-suffix 'transient--do-call
["Arguments"
("-e" "Exclude" "--exclude")]
("e" "Exclude" "--exclude")
("~" "Fuzzy" "--fuzzy")]
["Search"
("T" "Title" calibre-library-search-title)
("a" "Author" calibre-library-search-author)
@ -121,13 +138,26 @@ ARGS is the argument list of a transient command."
(interactive)
(setf calibre-search-composing-filter nil))
(defun calibre-search--make-composite-filter-component (field val fuzzy)
"Create a component of a composite search filter.
FIELD is a symbol identifying the field to search. VAL is the
value to search for. If FUZZY is non-nil create a fuzzy filter."
(if fuzzy
`[,field ,val ~]
`[,field ,val]))
(defmacro calibre-search--composition-function (field)
"Create a function adding a filter for FIELD to a composite filter."
`(defun ,(intern (format "calibre-search-compose-%s" field)) ()
`(defun ,(intern (format "calibre-search-compose-%s" field)) (val &optional args)
,(format "Add a filter for %s to the composite filter under construction." field)
(interactive)
(setf calibre-search-composing-filter
(cons (vector ',(intern field) (,(intern (format "calibre-search-chose-%s" field)))) calibre-search-composing-filter))))
(interactive (list (,(intern (format "calibre-search-chose-%s" field)))
(transient-args 'calibre-search-compose)))
(push (calibre-search--make-composite-filter-component
(quote ,(intern field))
val
(calibre-search--fuzzy-search-p args))
calibre-search-composing-filter)))
(calibre-search--composition-function "title")
(calibre-search--composition-function "author")
@ -140,7 +170,8 @@ ARGS is the argument list of a transient command."
"Create a composite filter."
:transient-suffix 'transient--do-call
["Arguments"
("-e" "Exclude" "--exclude")]
("-e" "Exclude" "--exclude")
("~" "Fuzzy" "--fuzzy")]
["Compose"
("T" "Title" calibre-search-compose-title)
("a" "Author" calibre-search-compose-author)

View File

@ -9,7 +9,7 @@ This manual is for calibre.el (version @value{VERSION},
@value{UPDATED}), a package for interacting with Calibre libraries from
Emacs.
Copyright @copyright{} 2023 Free Software Foundation, Inc.
Copyright @copyright{} 2023,2024 Free Software Foundation, Inc.
@quotation
Permission is granted to copy, distribute and/or modify this document
@ -241,11 +241,24 @@ it must be one of:
@end itemize
@var{VALUE} is the string to compare @var{FIELD} to.
@cindex fuzzy filter
For a given book to match against a basic filter the value of
@var{FIELD} for that book must be exactly equal to @var{VALUE}.
Sometimes this exact matching is undesirable and you would like to find
all books where @var{FIELD} contains @var{VALUE} instead. This is
exactly what fuzzy filters do.
A fuzzy filter is a vector @code{[@var{OP} @var{FIELD} @var{VALUE} ~]}.
@var{OP}, @var{FIELD}, and @var{VALUE} have the same meaning as in a
basic filter. The @code{~} is what identifies the filter as fuzzy.
@cindex composite filter
A composite filter, is a vector @code{[@var{OP} @var{FILTERS}]} where
@var{OP} has the same meaning as for basic filters and @var{FILTERS} is
a list of vectors @code{[@var{FIELD} @var{VALUE}]}. In each such vector
@var{FIELD} and @var{VALUE} have the same meaning as for a basic filter.
a list of vectors @code{[@var{FIELD} @var{VALUE}]} or @code{[@var{FIELD}
@var{VALUE} ~]}. In each such vector @var{FIELD} and @var{VALUE} have
the same meaning as for a basic filter and a @code{~} at the end marks
that particular component as fuzzy.
@node Modifying your library
@chapter Modifying your library

View File

@ -1,6 +1,6 @@
calibre.el NEWS -*- outline -*-
Copyright (C) 2023 Free Software Foundation, Inc.
Copyright (C) 2023,2024 Free Software Foundation, Inc.
See the end of the file for license conditions.
This file is about changes in calibre.el, the Emacs client for
@ -8,6 +8,11 @@ Calibre.
* Changes in calibre.el 1.4.0
** Enable fuzzy searching
The search interface now supports 'fuzzy' searching, which matches any
book whose value for the search field contains the search term as a
substring.
** Allow opening books in external programs
The new calibre-library-open-book-external command allows opening
books using an external program.