Using consult and org-ql to search my Org Mode agenda files and sort the results to prioritize heading matches

| emacs, org

I want to get better at looking in my Org files for something that I don't exactly remember. I might remember a few words from it but not in order, or I might remember some words from the body, or I might need to fiddle with the keywords until I find it.

I usually use C-u C-c C-w (org-refile with a prefix argument), counting on consult + orderless to let me just put in keywords in any order. This doesn't let me search the body, though.

org-ql seems like a great fit for this. It's fast and flexible, and might be useful for all sorts of queries.

I think by default org-ql matches against all of the text in the entry. You can scope the match to just the heading with a query like heading:your,text. I wanted to see all matches, prioritize heading matches so that they come first. I thought about saving the query by adding advice before org-ql-search and then adding a new comparator function, but that got a bit complicated, so I haven't figured that out yet. It was easier to figure out how to rewrite the query to use heading instead of rifle, do the more constrained query, and then append the other matches that weren't in the heading matches.

Also, I wanted something a little like helm-org-rifle's live previews. I've used helm before, but I was curious about getting it to work with consult.

Here's a quick demo of my-consult-org-ql-agenda-jump, which I've bound to M-s a. The top few tasks have org-ql in the heading, and they're followed by the rest of the matches. I think this might be handy.

Figure 1: Screencast of using my-consult-org-ql-agenda-jump
(defun my-consult-org-ql-agenda-jump ()
  "Search agenda files with preview."
  (let* ((marker (consult--read
                  :state (consult--jump-state)
                  :category 'consult-org-heading
                  :prompt "Heading: "
                  :sort nil
                  :lookup #'consult--lookup-candidate))
         (buffer (marker-buffer marker))
         (pos (marker-position marker)))
    ;; based on org-agenda-switch-to
    (unless buffer (user-error "Trying to switch to non-existent buffer"))
    (pop-to-buffer-same-window buffer)
    (goto-char pos)
    (when (derived-mode-p 'org-mode)
      (org-fold-show-context 'agenda)
      (run-hooks 'org-agenda-after-show-hook))))

(defun my-consult-org-ql-agenda-format (o)
   (org-ql-view--format-element o)
   'consult--candidate (org-element-property :org-hd-marker o)))

(defun my-consult-org-ql-agenda-match (string)
  "Return candidates that match STRING.
Sort heading matches first, followed by other matches.
Within those groups, sort by date and priority."
  (let* ((query (org-ql--query-string-to-sexp string))
         (sort '(date reverse priority))
         (heading-query (-tree-map (lambda (x) (if (eq x 'rifle) 'heading x)) query))
          (mapcar #'my-consult-org-ql-agenda-format
                  (org-ql-select 'org-agenda-files heading-query
                    :action 'element-with-markers
                    :sort sort)))
          (mapcar #'my-consult-org-ql-agenda-format
                  (org-ql-select 'org-agenda-files query
                    :action 'element-with-markers
                    :sort sort))))
     (seq-difference all-matches matched-heading))))

(use-package org-ql
  :bind ("M-s a" . my-consult-org-ql-agenda-jump))

Along the way, I learned how to use consult to complete using consult--dynamic-collection and add consult--candidate so that I can reuse consult--lookup-candidate and consult--jump-state. Neat!

Someday I'd like to figure out how to add a sorting function and sort by headers without having to reimplement the other sorts. In the meantime, this might be enough to help me get started.

This is part of my Emacs configuration.
You can comment with Disqus or you can e-mail me at