d12frosted / vulpea

A collection of functions for note taking based on `org` and `org-roam`.
GNU General Public License v3.0
242 stars 12 forks source link

Query all nodes that contain links to nodes A __and__ B #105

Closed wuqui closed 3 years ago

wuqui commented 3 years ago

I'm very interested in being able to query my org-roam DB like with org-ql or logseq. I came across your package vulpea, but I didn't find any documentation on how to query the database, ideally beginner-friendly?

For example, I would like to do queries that return a list of all nodes that link to nodes A and B. Is something like this possible?

d12frosted commented 3 years ago

Hey @wuqui

Right now links are not part of vulpea-note hence vulpea-db-query will not help here. Reason for that is simple - I never needed, this but if someone needs, that I am totally fine adding them (considering performance penalty is neglectable).

So back to your specific question. This library already provides a function to find notes linked to current note - vulpea-find-backlink. If we look into its implementation, we can see that it uses org-roam-backlinks-get:

https://github.com/d12frosted/vulpea/blob/d6792e95c499a2ee85b0d8b11295b61777a46038/vulpea.el#L95-L100

So we can just combine it into solution thanks to existence of seq-intersection. Let's say we want to find all notes that link note for Corinto grape (id feffc6de-9a35-449b-8470-e64b4f28832c) and Nero d'Avola (id c9731b65-61f8-4007-9dbf-d54056f55cc4). For simplicity it's going to show titles of notes that link them both.

(let* ((node1 (org-roam-populate
               (org-roam-node-create :id "feffc6de-9a35-449b-8470-e64b4f28832c")))
       (backlinks1 (seq-map #'org-roam-backlink-source-node
                            (org-roam-backlinks-get node1)))
       (node2 (org-roam-populate
               (org-roam-node-create :id "c9731b65-61f8-4007-9dbf-d54056f55cc4")))
       (backlinks2 (seq-map #'org-roam-backlink-source-node
                            (org-roam-backlinks-get node2))))
  (seq-intersection
   (seq-map #'org-roam-node-title backlinks1)
   (seq-map #'org-roam-node-title backlinks2)))

This returns the following list for me (first note is a description of wine that contains both grapes and the latter where I mention everything in one single journal note):

("Tenuta di Castellaro Nero Ossidiana 2015" "Saturday, 23 October 2021")

As opposed to this list, when I combine these two lists via seq-uniq:

("Tenuta di Castellaro Nero Ossidiana 2015" "Tenuta di Castellaro Corinto 2017" "Saturday, 23 October 2021" "Cerasuolo di Vittoria DOCG" "Frappato" "Donnafugata Tancredi 2016" "Tasca Regaleali Nero d'Avola 2017" "Firriato Santagostino Baglio Soria Nero d'Avola Syrah 2014" "COS Pithos Rosso 2010" "Gulfi NeroSanlorè 2013" "Gulfi neroBaronj 2016" "Gulfi Nerojbleo 2009" ...)

As you can see, none of vulpea functions are used here. Because org-roam is powerful enough by itself. If backlinks are first class citizen in vulpea (and again, if someone has the need for this I would take a look how to add them), the code would look like this:

(vulpea-db-query
 (lambda (note)
   (and (seq-contains-p (vulpea-note-backlinks note)
                        "feffc6de-9a35-449b-8470-e64b4f28832c")
        (seq-contains-p (vulpea-note-backlinks note)
                        "c9731b65-61f8-4007-9dbf-d54056f55cc4"))))

Hope that helps.

wuqui commented 3 years ago

Brilliant, thanks for this quick and excellent answer!

How could I turn this into an (interactive) function I can use? So from a user perspective, for this to be practically useful, you would probably want to be able to call a function and then interactively select >= 1 node and get a list of notes back?

I think this should be incredibly useful for lots of people in different use cases. I guess the fact that queries like this are very popular in Roam Research and logseq would be some empirical evidence that it's worth building something like this for org-roam.

d12frosted commented 3 years ago

Brilliant, thanks for this quick and excellent answer!

Glad you find it useful.

get a list of notes back

What do you mean by 'back'? :) Like, in interactive use there is no point in returning anything from a function as it's interactive use. But let me show you several examples.

First, here is a function that returns a list of backlinks for a given list of notes.

(defun vulpea-backlinks-many (notes)
  "Return notes that link to all NOTES at the same time."
  (let* ((blinks-all
          (seq-map
           (lambda (note)
             (seq-map
              #'org-roam-backlink-source-node
              (org-roam-backlinks-get
               (org-roam-populate
                (org-roam-node-create :id (vulpea-note-id note))))))
           notes)))
    (seq-reduce
     (lambda (r e)
       (seq-intersection
        r e
        (lambda (a b)
          (string-equal (org-roam-node-id a)
                        (org-roam-node-id b)))))
     blinks-all
     (seq-uniq (apply #'append blinks-all)))))

Non-interactive usage example:

ELISP> (seq-map
        #'vulpea-note-title
        (vulpea-backlinks-many
         (list
          (vulpea-db-get-by-id "feffc6de-9a35-449b-8470-e64b4f28832c")
          (vulpea-db-get-by-id "c9731b65-61f8-4007-9dbf-d54056f55cc4"))))
("Tenuta di Castellaro Nero Ossidiana 2015" "Saturday, 23 October 2021")

Now we ca use it in many ways. For example, this will allow to interactively select several notes, and then select a title among these titles:

(defun select-backlinks-many ()
  "It's hard to explain."
  (interactive)
  (let* ((notes (vulpea-utils-collect-while
                 #'vulpea-select
                 nil
                 "Note" :require-match t))
         (blinks (vulpea-backlinks-many notes)))
    (completing-read
     "Blacklink: "
     (seq-map #'vulpea-note-title blinks))))

It uses a helper function from vulpea:

https://github.com/d12frosted/vulpea/blob/d6792e95c499a2ee85b0d8b11295b61777a46038/vulpea-utils.el#L89-L111

Basically, you initially select as many notes as you wish and stop this by pressing C-g, and then it will prompt for backlinks.

If you wish, I can send an example where you select and visit backlink instead of just returning the name 😸