Logseqのクエリで複合検索や逆順検索をする

先に結論

TODOの最新30を取得し、優先度 (A,B,C) をアルファベット順、日付を降順(新しい順)にする例。

(記事の後半に別解も紹介。)

#+BEGIN_QUERY
{:title "TODO (直近30)"
 :query (todo TODO)
 :result-transform (fn [result]
  (let [rev-pri (fn [p] (case p "A" "Z" "B" "Y" "C" "X" "Z" "B" "A"))]
   (->> result
    (sort-by (fn [r]
        [
          (rev-pri (get r :block/priority "Z"))
          (get (get r :block/page) :block/journal-day 19000101)
        ]
     ))
    reverse
    (take 30)
    ;; 日付デバッグ用
    ;; (map (fn [m] (update m :block/properties
    ;;   (fn [u]
    ;;      (assoc u
    ;;           :journal-day (get (get m :block/page) :block/journal-day)
    ;;       )
    ;;   ))))
    )))
 :breadcrumb-show? false
}
#+END_QUERY

解説

まず、->> はThread-Lastマクロと呼ばれていて、関数を順番に実行して適用するという意味。Thread-Firstマクロ (->) との違いは、引数が関数の最後に渡されるか最初に渡されるかの違い。

わかりやすくいえば次のように置き換えられる。

(->> result fn_a fn_b fn_c)
; 上と下は同じ意味
(fn_c (fn_b (fn_a result)))

もう少し具体的な例をClojureDocから引用しておく。

;; An example of using the "thread-last" macro to get
;; the sum of the first 10 even squares.
user=> (->> (range)
            (map #(* % %))
            (filter even?)
            (take 10)
            (reduce +))
1140

;; This expands to:
user=> (reduce +
               (take 10
                     (filter even?
                             (map #(* % %)
                                  (range)))))
1140

これを踏まえると、:result-transform の中身は次のように分けて考えられる。

(fn [result]
  ; 先に使いたい関数を定義
  (let [rev-pri (fn [p] (case p "A" "Z" "B" "Y" "C" "X" "Z" "B" "A"))]
   ; 得られた結果を、
   (->> result
    ; まずソートして
    (sort-by (fn [r]
        [
          (rev-pri (get r :block/priority "Z"))
          (get (get r :block/page) :block/journal-day 19000101)
        ]
     ))
    ; 逆順にして
    reverse
    ; 30個とってきて、
    (take 30)
    ; ついでに表示データに :journal-day を加える (※デバッグ用で、必須ではない。)
    (map (fn [m] (update m :block/properties
      (fn [u]
         (assoc u
              :journal-day (get (get m :block/page) :block/journal-day)
          )
      ))))
    )))

わかりやすく、ソートの部分だけを取り出しておく。

(sort-by (fn [r]
    [
      (rev-pri (get r :block/priority "Z"))
      (get (get r :block/page) :block/journal-day 19000101)
    ]
 ))

ここでは、複数のソート条件を配列で渡している。(ちなみにget関数の最後の引数にあるZ19000101は、フォールバックというもので、見つからなかったときにその文字列で間に合わせるというやつ。)

ちなみに、sort-byreverseを2回やってしまうと、結果全体が新規に書き換わってしまうのでうまくいかない。Logseqフォーラムにもあるように、juxt等も同様の理由で今回は役に立たない。

そこで、ここでは作為的に、rev-priなる、優先順位のアルファベットを逆転させる関数を先にletで定義して、A->Z, B->Y... と先に置き換えておくことで、逆順ソートを実現している。

Thread-Lastマクロの最後で、結果を逆転=reverseしているので、最終結果としては、優先度 (A,B,C) がアルファベット順、日付が降順(新しい順) になるようにソートされる。

なお、今回は「優先度」を配列の先に置いているので、「優先度」が優先されてソートされるが、逆にすれば日付が優先される。これは例えばDONEなどの終わったものを順に表示させるときに良いと思う。

ちなみに今回はsort-byに配列を渡したけれど、配列ではなくて文字列結合(str)でも可。こちらのほうが内部で何が起きているかわかりやすいと思う。

別解

journal-day20241001のような数値で返ってくることを活かして、

(sort-by (fn [r]
    [
      (get r :block/priority "Z")
      (- (get (get r :block/page) :block/journal-day 19000101))
    ]
 ))

として reverse を取り除いても良い。

ただこの方法はマイナス値として扱えるような数値 (つまり整数) が来た場合に限る。

余談

複合検索とか、逆順検索みたいなのは、標準機能としてあってくれてもいいかなーと若干思う。

ただ、アルファベットを数値にする方法がわかれば1、例えば日付の逆順なんかはマイナス値にすれば良いので、より汎用的な方法はありそう。(ただ、数値とかアルファベット1文字とかじゃない、2024-10-01 みたいな非数値の文字列が現れたときはたぶん悩む。)

(追記)LogseqフォーラムにFeature Requestも一応出してみた。もしかしたらより良い方法があって教えてもらえるかも。2

  1. cljsなのでint関数は使えないし、Logseqは使える関数が制限されている様子。「優先度」(A,B,C) のように特定の文字列であれば、数値に変換できる範囲に変換してからread-stringする等もできる…?

  2. ただ、執筆時点現在LogseqはDB版という新しい機構に開発リソースを割いている様子なので、もしそちらが公開されればクエリなどの方法も刷新されそう。その際はこの議論は古くなってしまう可能性も。