How to query for indirect page links with certain depth?

Hello, is it somehow possible to use advanced queries to get all indirect/transitive links for a page?

image

Example content for page “Knowledge Graph”:

uses:: [[Graph (datastructure)]]

Query [[Graph (datastructure)]] would return all direct links, Knowledge Graph here. I’d like to have Logseq to be returned as well. As result can grow quickly, some attribute like max depth would be useful to provide.

This may help you along for your answer:
https://qwxlea.github.io/#/page/62b0345a-34d4-4d0c-a847-8a6489f5fdb6
This searches recursively.
In this case specifically for TODO items.
Let me know if you need more help/have further questions. I can get to them when I have a bit more time.

1 Like

would you consider using the logseq-graph-analysis plugin (or the page graph) ?
I often lookup indirect links with it (search then toggle filter, set n-hops to 1)

1 Like

Wow, this is tricky. I think I have understood that recursive query roughly, but needed to have a look at yet another advanced query tutorial only for this construct :sweat_smile:

That would be great! Much appreciated.

Thanks for the hint! I tried that plugin directly before. Overall the features seem to be great. Though I am not satisfied with overall graph widget performance (vanilla and plugin), even on idle. My next thought then was: “Why not get near nodes directly via query engine?”

Vanilla page graph (the one in the sidebar) would be an interesting alternative, but to my knowledge, it only can display direct neighbors and has no depth-level slider.

So this… in theory… works…

#+BEGIN_QUERY
{
 :query [:find (pull ?p [*])
         :in $ %
         :where
         [?c :block/name "bewegen"]
         (pr ?p ?c)
       ]
 :rules [
         [(pr ?p ?c)
               [?c :block/tags ?p]
          ]
         [(pr ?p ?c)
               [?c :block/tags ?t]
               (pr ?p ?t)
         ]
           ]
}
#+END_QUERY

There is no max depth though and it will return just a long list of pages.
I don’t know how helpful that is tbh.
For example in my graph I have:

  • areas
    • fysieke gezondheid
      • bewegen

The above query returns fysieke gezondheid and areas. It is basically a slight modification of the query I send earlier.
However it isn’t capped. I know you could manually cap it along the lines of:

#+BEGIN_QUERY
{
 :query [:find (pull ?gp [*])
         :where
         [?c :block/name "bewegen"]
         [?c :block/tags ?p]
         (or [?c :block/tags ?gp]
               [?p :block/tags ?gp])
       ]
}
#+END_QUERY

Sorry that I do not have a better answer than this.

Also this assumes the use of tags:: for linking pages.
So bewegen would have tags:: [[fysieke gezondheid]]
And fysieke gezondheid in turn would have tags:: [[areas]]

EDIT: Wait… you were asking for the reverse lol… I’ll go edit the queries.
EDIT: Changed it up a bit :slight_smile: should find parents/grandparents/etc. now.

1 Like

Thank you so much! I certainly will have something to start and play around now.
I also begin to question, whether this will be practical or rather academical :smiley:.

My hidden end goal was to consider both incoming and outcoming connections . And filter by multiple page links. But again same conclusion as above :wink: . The best probably would be a wizard query builder or a more performant graph widget.

1 Like

I have now created a simple recursive query, that collects adjacent nodes in the graph. But if I don’t pay attention with the notes, it will result in an infinite loop and freeze Logseq lol. Example of pages A, B, C, D referencing each other in a cycle:

A -> B
^    |
|    v
D <- C

Already visited pages would need to be saved in a collection to not re-process them. Is it somehow possible within Logseq Datomic query dialect to create a temporary list or set and append elements to this list? Checking for existence then should be possible with contains?.

In a total naive way, I tried this:

[#{} ?mylist]
[(conj ?mylist ?p)]
# or
[(clojure.core/conj ?mylist ?p)]

, and one encountered problem is that conj seems to be not available:

“Unknown predicate 'conj in [(conj ?mylist ?p)]”

“Unknown rule 'conj in (conj ?mylist ?p)”

Hello. I found the query you made before the edit a lot more useful! I also made it so that the “areas” page itself is included in the results. However, I can’t seem to replace “areas” with ?current in the :rules clause (see query below). It’s like the binding of ?current to the current page as per the :inputs clause doesn’t carry over to the :rules clause but it does it the :where clause. Weird… Can you help out? Ideally, the query would automatically reference the current page without my having to specify the page name in it. Thanks!

Hi, I’ve tried replacing “areas” with ?current to reference the current page as I did in the :where clause. However, the binding of ?current to the current page as per the :inputs clause doesn’t seem to carry over to the :rules clause! I don’t understand the inconsistency :frowning_with_open_mouth: Thanks in advance for any help.

#+BEGIN_QUERY
{
:query [:find (pull ?p [*])
:in $ ?current %
:where
[?a :block/name ?current]
(pr ?a ?p) 
]
:inputs [:current-page]
:rules [
[(pr ?p ?c)
[?c :block/tags ?p]
]
[(pr ?p ?c)
[?c :block/tags ?t]
(pr ?p ?t)
]
[(pr ?p ?c)
[?c :block/name "areas"]]
]
}
#+END_QUERY
#+BEGIN_QUERY
{
:query [:find (pull ?p [*])
:in $ ?current %
:where
[?a :block/name ?current] ;this sets ?a to the current page.
(pr ?a ?p) ;this is the input for the rules clause. We see ?a is already an input
]
:inputs [:current-page]
:rules [
[(pr ?p ?c) ;just like with inputs and in, here we see this rule gets ?a and puts it in ?p
[?c :block/tags ?p] ;so this means that the page you are looking for is tagged with the current page
]
[(pr ?p ?c) ;same idea here
[?c :block/tags ?t] ;the page is tagged with ?t
(pr ?p ?t) ;the current page and ?t and then entered into the rule for checking
]
[(pr ?p ?c)
[?c :block/name "areas"]] ;this rule needs to change. We need a simple [(= ?c ?p)]
]
}
#+END_QUERY

I’ve added some comments to your query to explain :slight_smile:

Thank you so much for taking the time! It helps a lot. Here is the corrected query with your recommendation at the very end. I must be missing something. It’s giving me a “Query failed: Insufficient bindings: #{?p} not bound in [(= ?p ?a)]”

#+BEGIN_QUERY
{
:query [:find (pull ?p [*])
:in $ ?current %
:where
[?a :block/name ?current] ;this sets ?a to the current page.
(pr ?a ?p) ;this is the input for the rules clause. We see ?a is already an input
]
:inputs [:current-page]
:rules [
[(pr ?p ?c) ;just like with inputs and in, here we see this rule gets ?a and puts it in ?p
[?c :block/tags ?p] ;so this means that the page you are looking for is tagged with the current page
]
[(pr ?p ?c) ;same idea here
[?c :block/tags ?t] ;the page is tagged with ?t
(pr ?p ?t) ;the current page and ?t and then entered into the rule for checking
]
[(pr ?p ?c)
[(= ?c ?p)]]
]
}
#+END_QUERY

The error confused me for a bit there :sweat_smile:
But it’s because ?c is undefined.
The rule should be:

  [(pr ?p ?c)
   [?c :block/name]
   [(= ?c ?p)]
  ]

Edit: for performance this is probably better

  [(pr ?p ?c)
   [?p :block/name ?nm]
   [?c :block/name ?nm]
  ]

Thank you so much! That was the final thing I needed to finalize the query. I present to you my own idea of a query for task management, returning TODO blocks tagged with a given page OR subpages (defined through tagging). Say you felt like doing some #housekeeping that day, you go on the “housekeeping” page and see as TODOS all the tasks tagged with #laundry, #dishes, etc. that have to do with housekeeping more generally. Or you wanted to study some #philosophy and get better in this field, you’d get every tasks related to #ethics, #ontology, #epistemology, etc., provided they were themselves tagged with #philosophy. At any point in the hierarchy, the query would encapsulate the subconcepts, as long as they are directly or indirectly linked through tags. The ability to represent a hierarchy or poly-hierarchy of concepts is super important to me in task management. It perfectly mimics the brain and it’s tendency to nest concepts within larger concepts so that it can function properly with minimal effort. How many times have we found ourselves navigating conceptual hierarchies in our minds: “Let me improve my #piano playing today” VS “Let me work specifically on #jazz_piano” today” or “Do some #school work” VS “Study for this #specific_course” or even “Get ahead at #work” VS “Tackle this #specific_area_at_work” and so on and so forth. Feel free to add the query below to “Queries for tasks management” if you think it might be useful to others. I’m happy to contribute.

#+BEGIN_QUERY
{
:query [:find (pull ?b [*])
       :in $ ?current %
       :where
       [?a :block/name ?current]
       (pr ?a ?p)
[?b :block/refs ?p]
[?b :block/marker ?marker]
       [(contains? #{"TODO"} ?marker)]
]
:inputs [:current-page]
:rules [
       [(pr ?p ?c)
             [?c :block/tags ?p]
        ]
       [(pr ?p ?c)
             [?c :block/tags ?t]
             (pr ?p ?t)
       ]
[(pr ?p ?c)
 [?p :block/name ?nm]
 [?c :block/name ?nm]
]
         ]
}
#+END_QUERY

Is there be a way to adjust this query so that it’s looking at a specific property, say “type”, instead of generic tags? I want to learn this language so bad but it’s so hard… Thank you so much.

Yes. Yes it is. It is a pain to initially come to terms with it and then to learn its ins and outs. Heck I’m still learning!

Yes. So tags is a specific tags:: syntax. It gets interpreted as always being page references. “Normal” properties are not.
This complicates things a little bit. There is a setting in config to make any property behave in the same way as tags do (that is, always as a page reference).
For queries there’s 2 options, complex and a little bit easier.
I’ve actually switched to the simpler solution as it then doesn’t matter whether the value is an actual reference or just text. However you cannot get the value of the property that way.

There’s plenty of example of the complex syntax. I wrote a bunch in different threads lol.

For the simple solution you would write instead of [?b :block/tags ?value], (property ?b :type ?value)
This is simple query syntax that Logseq translates to the more complex variant.

I hope that helps! And feel free to link to this topic in queries for task management if you want :slight_smile:

Thank you so much for your help. So I can’t seem to convert the query I have using a hierarchy with tags to one that uses a hierarchy with property chains that allow for transitive relationships. In my specific example, I want to navigate hierarchies using the :type property. [[labrador]] is a type of [[dog]] which is a type of [[animal]] and so on. I want to query for type:: [[animal]] and get back all blocks in which any of the subsets appear in the :type property. Tried replacing all instances of [?variable :block/tags ?value] with (property ?variable :type ?value) but it’s not working at all… Please help at your convenience if you’re willing of course :frowning: and let me know if it’s too much. I don’t want to abuse your kindness! Thank you so much.

Well oops. My bad. This apparently is not something that’s been implemented.
Now with proper testing instead of just from memory!

#+BEGIN_QUERY
{:title [:b "Find related blocks"]
 :inputs [:current-page]
 :query [:find (pull ?b [*])
   :in $ ?current %
   :where
    [?in :block/name ?current]
    (pr ?in ?out)
    [?b :block/refs ?out]
    [?b :block/marker ?marker]
    [(contains? #{"TODO"} ?marker)]
 ]
 :rules [
  [(pr ?in ?out) ; the basis of the search give us the ?out that fits with the ?in through its type.
    [?in :block/original-name ?name]
    [?out :block/properties ?prop]
    [(get ?prop :type) ?type]
    [(contains? ?type ?name)]
  ]
  [(pr ?in ?out) ; page ?t has the property value we want, but we continue to look for ?out this time using ?t as the new ?in.
    [?in :block/original-name ?name]
    [?t :block/properties ?prop]
    [(get ?prop :type) ?type]
    [(contains? ?type ?name)]
    (pr ?t ?out)
  ]
  [(pr ?in ?out) ; ?in and ?out are the same page as name is unique.
    [?in :block/name ?nm]
    [?out :block/name ?nm]
  ]
 ]
}
#+END_QUERY

@Siferiax
How to change the behavior in config.edn to make all properties behave like tags?

There’s no option to change that default actually. You can only change the option on a property basis.
However, [[value1]], [[value2]] will always work in a similar way.

1 Like

Thank you so much it works perfectly!

1 Like

Related to the solution you proposed for setting up a hierarchy using property values, is there any way to set up a hierarchy using the properties themselves? That would be insane. How would we set up, for example, beneficiary:: to encompass both student:: and recipient:: in the following example:

TODO Give Katja a piano lesson
student:: Katja

TODO Send an email to Katja
recipient:: Katja

Querying blocks with property «beneficiary:: Katja» (as in Katja being the beneficiary of a given task) would essentially be querying for all of its sub-properties and return both items above. I would turn on property-pages in config.edn and add page-property «type:: beneficiary» to both «recipient» and «student».

Thank you for your continuous support! Maybe I should move to Tana. I feel like I’m trying too hard to get hierarchical structure out of Logseq :sweat_smile:

1 Like

Maybe, I can’t say. I have never used Tana lol. Logseq gives me everything I need, so I have no incentive to look further.

Anyway, it all depends on your needs. For example, if you need to find tasks for Katja, you might also consider making Katja a page and then finding all tasks that reference that page.

But Logseq is as you also conclude, not really designed for strict hierarchy. And so does not offer any innate support for it.