Nearby Neighbors Plugin: POC release

In #5638 we discussed a plugin idea I had, to create a “what’s nearby” footer based on the implicit connections in links, tags, and fields.

I now have a POC of this idea at Nearest Neighbors Demo — adding nearby neighbors footer (source code: https://github.com/CrossEye/TW5-Nearby.)

I’m going to have questions about how to continue, but it’s bedtime now. For the moment, any feedback would be welcome. If you have answers to any questions I raised in the main tiddler’s TODO section, I would be especially grateful.

You can try it out by installing the plugin on a local copy of https://tiddlywiki.com/, and opening, say, the “abs Operator” tiddler. It should add a footer something like this:

As I said, this is a very early proof-of-concept. Many things are still in flux, but feedback would be welcome.

5 Likes

Terrific stuff :medal_sports:

Found a buglet - if you click on a non-existent link you get an RSOE, and interestingly trying to close the non-existing placeholder tid doesn’t quite work - as below:

firefox_PDD7rF3Gmr

1 Like

One thing only. Though I hope will not “foul your footpath” (make things difficult) to sort it?

At the moment all tiddlers get this, even IF they have NO “Nearby” entries … for example …

That is bad.

I dunno how difficult it would be to ensure that when there are NO “Nearby” tiddlers the dropdown would not ever appear??

A comment (otherwise very enthusiastic), TT

If you override $:/plugins/ScottSauyet/Nearby/ViewTemplate like this both issues above are resolved:

<$list filter="[all[current]is[tiddler]]">
	<div class="nearby-neighbors">
	<hr/>
	<details>
	<summary>Nearby</summary>
	<<list-links filter:"[all[current]neighbors[10]]">>
	</details>
	</div>
</$list>
4 Likes

I wanted to render things like Node Explorer from Shiraz 2.7.2 — create stylish contents in Tiddlywiki - this was easy (thanks @Mohammad ):

\define subtiddler-pattern() $(currentTiddler)$/
<style>
.nearbyItem {
	font-size: 0.8rem;
	border: 1px solid red;
	padding-left: 3px; 	padding-right: 3px;
	padding-top: 1px; padding-bottom: 1px;
	margin-bottom: 5px;
	border-radius: 5px;
	background-color: red;
	color: white !important;
	text-decoration: none;
	display: inline-block;
}
.nearbyIcon {
	vertical-align: middle;
	margin-bottom: 2px;
}
.nearby-neighbors {
	border-top: 1px solid lightblue;
	padding-top: 6px;
}
</style>
<$list filter="[all[current]is[tiddler]]">
	<div class="nearby-neighbors">
		[img height=18px class=nearbyIcon [Nearby|$:/njb/images/nearby]]
			<$list filter=[all[current]neighbors[10]] variable="currentRecord">
				<$let type={{{ 
					[all[current]links[]match<currentRecord>then[info text-white]]
					[all[current]backlinks[]match<currentRecord>then[danger text-white]]
					[all[current]tagging[]match<currentRecord>then[warning text-dark]]
					[<currentRecord>prefix<subtiddler-pattern>then[primary text-white]]
					:else[<currentRecord>search:text<currentTiddler>then[secondary text-white]else[dark text-white]] 
					}}} >
						<$link to=<<currentRecord>> class="nearbyItem" />
				</$let>
		</$list>
	</div>
</$list>

Node Explorer output above the line, Nearby below:

3 Likes

Although I hadn’t tested this yet, I suspected as much. I knew I’d have to work on the actual footer. And several other responses already have fixes for it. I will update this evening.

Thank you!

Yes, I know about that and will fix it soon. It’s a fairly trivial fix. I probably should have delayed a day and fixed issues like this before announcing.

Thanks for the report!

1 Like

Thank you. I knew it was going to be simple, but didn’t realize just how simple. Nice!

Interesting. I think I like the outline and background color on there – although that red would not be my choice – and I definitely like the inline-block rather than block mode for the list. In the end, I’m expecting to have an entirely different view here, some sort of graphic visualization that I haven’t come even close to working out yet.

One thing confuses me. It seems like you go to a lot of work to define type, and then never use it. Is there some automagical work that makes <$link ... /> respond to type? Or is this just copy-paste detritus? Or something else?

Again, thank you very much for your help!

1 Like

Update 2023-01-04: In a later post, this first draft has been significantly reworked and extended.
The final solution can be found here: Nearby Neighbours: A WikiText Solution - Tips & Tricks - Talk TW (tiddlywiki.org)

Hi Scott,
The Nearby Neighbors is a pretty neat idea. I was curious how hard it would be to do something like it in WikiText, based on your metric idea from the other thread, and came up with this:

\define relatedWeight() 10
\define taggedWeight() 3
\define sharedTagWeight() 5
\define linkedWeight() 4

\define numberOfTiddlersShown() 25

\define relatedScore() [enlist<relatedTiddlers>match<currentTiddler>] +[then<relatedWeight>else[0]]
\define taggedScore() [enlist<taggedTiddlers>match<currentTiddler>] +[then<taggedWeight>else[0]]
\define sharedTagScore() [enlist<sharedTagTiddlers>match<currentTiddler>] +[then<sharedTagWeight>else[0]]
\define linkedScore() [enlist<linkedTiddlers>match<currentTiddler>] +[then<linkedWeight>else[0]]

\define neighborScore() [reduce<relatedScore>] [reduce<taggedScore>] [reduce<sharedTagScore>] [reduce<linkedScore>] +[sum[]]

<$reveal tag="div" class="tc-tiddler-body" type="nomatch" stateTitle=<<folded-state>> text="hide">

<$set name="relatedTiddlers" filter="[all[current]listed[related]] [list[!!related]] +[!has[draft.of]]">
<$set name="taggedTiddlers" filter="[all[current]tags[]] [all[current]tagging[]] +[!tag[$:/tags/Journal]!is[system]!has[draft.of]]">
<$set name="sharedTagTiddlers" filter="[all[current]tags[]!is[system]tagging[]!has[draft.of]]">
<$set name="linkedTiddlers" filter="[all[current]links[]] +[!has[draft.of]]">
<$set name="neighbors" filter="[enlist<relatedTiddlers>] [enlist<taggedTiddlers>] [enlist<sharedTagTiddlers>] [enlist<linkedTiddlers>] -[all[current]]">

!! Howdy, neighbors!

	<ul>
		<$list filter="[enlist<neighbors>!sortsub:number<neighborScore>first<numberOfTiddlersShown>]">
			<li><$link /> (Score: <$text text={{{ [all[current]subfilter<neighborScore>] }}} />)</li>
		</$list>
	</ul>

</$set>
</$set>
</$set>
</$set>
</$set>

</$reveal>

Just tag a tiddler with this WikiText as $:/tags/ViewTemplate and you’ll get the list at the bottom of each tiddler. The score is only shown for debugging. If you wanted to only show the list when there actually are neighbors, just put everything inside the inner $set into a $list or $reveal which checks the neighbors list count.

It turned out to be fairly simple in the end. Some of the complexity is because I wanted to make it easier to amend and modify.
It does differ from @Scott_Sauyet’s original in two important ways:

  • Backlinks are not considered. I think calculating backlinks is quite resource intensive, especially for a large wiki with thousands of tiddlers. It can be added by adding a corresponding run to the linkedTiddlers filter expression.
  • It only regards the related field. I personally use this field to identify closely connected tiddlers in my wikis. The so connected tiddlers thus also get a very high metric. I haven’t thought about how to extend this to arbitrary / all fields, but this should be possible. I would exclude the list field, since that is often used for ordering and such and might result in extraneous high-ranking results (unless the associated metric is very low).

Curiously, I had to use the reduce operator in the neighborScore subfilter. That is purely to have access to a currentTiddler variable inside the sub-subfilter, which the subfilter or filter operators do not set (but sortsub does). I feel this is a bit of an inconsistency in the filter definitions. Also, I know of no straightforward way to check if some title is contained in a list that is stored in a variable – usually I would use the :intersection filter prefix, but I couldn’t get this to work in a subfilter definition. I’m sure @saqimtiaz has a solution or at least knows why that is.

Have a nice day
Yaisog

4 Likes

A bit of fine-tuning: To count the sharedTagWeight score for each shared tag, two lines must be changed:

\define sharedTagScore() [enlist:raw<sharedTagTiddlers>match<currentTiddler>] +[count[]multiply<sharedTagWeight>else[0]]
...
<$set name="sharedTagTiddlers" filter="[all[current]tags[]!is[system]] :map:flat[tagging[]!has[draft.of]] +[!is[blank]]">

This fills the sharedTagTiddlers list with one copy of the tiddler for each tag it shares, and then multiplies the number of occurences with the sharedTagWeight metric.

2 Likes

This is great!

People have assured me that this is doable in wiki text, but this is the first demonstration of the possibilities. I’m not sure that it will extend in the direction I want to go, which is to use these initial values (or mine, based on the reciprocals of these numbers) as the initial values in a shortest-path algorithm to give give a truer metric of distances.

I will definitely be learning from (stealing from? :wink:) your filters.

I don’t know what level of performance degradation I would accept to do it, but I would like to keep this like a real metric where dist(A, B) is always the same as dist(B, A). That’s the main reason I have dealt with backlinks here. It looks like there is already some caching going in in backlinks calculation, and that may help. I’m thinking that I will eventually add my own distance caching, but we’ll see.

Good call on the list field. I’ll exclude that one as well. For a general-purpose plugin, I think that all fields is a better approach than simply related, but I could certainly be convinced otherwise. One important point is that a wiki using many related fields already explicitly lists the information this plug-in is trying to extract implicitly; it probably simply wouldn’t need a plug-in like this.

I’m going to have to look at how this operator, um, operates. I want to relate it to JavaScript’s Array.prototype.reduce, but that may be leading me astray. And subfilters are still a mystery to me. I guess I’ll be checking that out.

Again, thank you very much. This is a big help.

One more relation that I missed (item 5 in Scott’s original list): Other tiddlers that are also tags of subordinate child tiddlers. Apart from manually filling the related field, these other tags of subordinate (journal) tiddlers indicate the closest* neighbors in my wikis. The more children have a shared other tag, the closer. The corresponding scores and lists are:

\define sharedTaggingWeight() 5
...
\define sharedTaggingScore() [enlist:raw<sharedTaggingTiddlers>match<currentTiddler>] +[count[]multiply<sharedTaggingWeight>else[0]]
...
\define neighborScore() [reduce<relatedScore>] [reduce<taggedScore>] [reduce<sharedTagScore>] [reduce<sharedTaggingScore>] [reduce<linkedScore>] +[sum[]]
...
<$set name="sharedTaggingTiddlers" filter="[all[current]tagging[]] :map:flat[tags[]!is[system]!has[draft.of]] +[!is[blank]]">
...
<$set name="neighbors" filter="[enlist<relatedTiddlers>] [enlist<taggedTiddlers>] [enlist<sharedTagTiddlers>] [enlist<sharedTaggingTiddlers>] [enlist<linkedTiddlers>] -[all[current]]">

Here again, multiple occurences of a neighbor count towards its score.

*Since in my templates, the tags of a tiddler (parents) and the list of tiddlers that is tagged with the current one (children) are always visible, these need not be duplicated in the neighbors list.

Have a nice day
Yaisog

2 Likes

Thanks for the POC @Scott_Sauyet and I continue to watch with a keen eye on your work. @Yaisog’s approach is similar to what I postulated earlier.

  • I am interested to see when the resulting list of nearby items differs from the relationships we already can leverage.
  • As you pointed out some wikis are repeat with relationships already and nearby is not as much use, but how would we describe wikis that will benefit from the nearby algorithm?
  • When listing such “relationships” can some metrics or methods be displayed to indicate how it came to be classified as such?
1 Like

Yes. If and when I do the Dijkstra’s algorithm work, I’ll be very interested to see if it can be reasonably ported to wiki-text!

As with all tools built on a base framework, it cannot do anything that the framework cannot already do, pretty much by definition. The question is whether it makes doing so simpler, more ergonomic, or in some way better. Standing on the shoulders of giants comes to mind here.

That’s an interesting question I’ll have to ponder. I have a use-case of my own, which is what’s prompting this, but I haven’t really considered what the general use-case would be.

Well I was hoping to display the shortest path value in some form, as text size in a word art, as edge length in a graph, as a value in a fixed color gradient. But I would not expect to explain directly that this value came from a tag and a link, and that one came from a shared tag, or any such. My thought is that this whole process is to consolidate that information into a single easily digestible view.

Thanks as always for the interesting insights!

2 Likes

Ok, understood, Keep going, I think you contribution will prove of great value.

I tried “Nearby Neighbours Plugin” on a copy of tiddlywiki.com which is already setup with a lot of relationships.

A small reflection; No need to respond, just make use of if helpful.

  • Many tiddlers may appear to be “only one step away” eg B is transcluded in this A, A is tagged with B.
    • I would be reluctant to choose one over the other as “closer”, and apply a weight.
  • I wonder if “first we eliminate the obvious” then “analyse the distance” to the not so obvious. The idea is identify all “directly connected” which the info tab and other tools already do, and exclude them from the nearby algorithm, returning “Nearby but you would not have known” items.
1 Like

The issues raised by @TiddlyTweeter and @NickB are resolved in a new version. The previous version is still available.

I’m hoping to have time tomorrow for more substantive issues.

1 Like

With this work in mind and a long held desire I am building a seperate set of tools for establishing a network of tiddlers, rather than a hierarchy like the TOC and “new hear” methods do.

In the near future I hope to have a set of test data that may prove useful for this project.

Is this needed? I assume there is no duplicate in sharedTagTiddlers to be kept!

Is it possible to show the relationship? For example, is the node is a tagged tiddler, or the one has the shared tag?

Nice work! We may categorize this in the group of node explorers (e.g. TZK, Mehregan, Shiraz Node Explorer, Stroll, TiddlyResearch, kin filter, …).

Some suggestions (ignore if not related)

S1.
Like above request, I like to know the relationship! So, if you add an option to let me turn on/off to show what is the relation of displayed node to current tiddler!

A dream would be to see nearby plugin output in a graph like Tidgraph. May be nearby could replace the old Tidgraph.

S2.
It would be great if there were some config tiddlers, allow me to display only selected categories! For example, if I do not want to show the linked nodes, or when I like to ignore all nodes tagged with the current tiddler.

Thank you for all your efforts and new plugins and tools you added to TW ecosystem.

1 Like