---
title: "Note Relations and Graph Views: Discover, Project, and Verify"
description: Trace ChatCrystal relation APIs, candidate discovery, and graph projections, keeping generated edges and tag co-occurrence within verified boundaries.
image: "https://zengliangyi.com/og/blog/en/note-relations-and-graph.png"
---

ChatCrystal exposes two kinds of graph data: directed relations between notes and the tag co-occurrence graph shown by the current interface. An edge is a reviewable clue produced by candidates, model output, or co-occurrence rules; it is not proof by itself.

This article follows the [relation service](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/relations.ts) and [relation routes](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/routes/relations.ts) at fixed commit [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8).

## Keep the two projections separate

The note relation table stores source, target, type, confidence, description, and `created_by`. Automatic discovery accepts eight enum values: `CAUSED_BY`, `LEADS_TO`, `RESOLVED_BY`, `SIMILAR_TO`, `CONTRADICTS`, `DEPENDS_ON`, `EXTENDS`, and `REFERENCES`. A given source-target pair can have only one relation of each type. Manual relations get a fixed confidence of 1, while `created_by` records their origin; the number is not a measured probability.

The current Web graph page calls `GET /api/graph/projection?level=tag`, which projects tag nodes and co-occurrence edges with a 120-node limit and a `minScore` strength filter. Clicking a tag loads up to 20 notes for that tag. `GET /api/relations/graph` still returns note nodes and relation edges, but it is a separate endpoint and is not necessarily what the current page displays.

## How automatic discovery selects candidates

`POST /api/notes/<note-id>/discover-relations` first runs semantic search with the source title, takes at most 20 candidates, and removes the source itself. If search fails or produces no candidates, it falls back to the 20 most recent notes. Candidate summaries are truncated to 200 characters; the source summary contributes at most 500 characters to the prompt. The candidate set is bounded, so discovery is not a scan of the entire library.

The model returns a structured array using the relation enum. The application keeps only candidate IDs with confidence at least `0.5`, then takes at most five relations. The schema describes a short description as “within 20 characters” but does not enforce that length. Persistence uses `INSERT OR IGNORE`, so an existing relation is not duplicated.

Replace `<note-id>` and `<relation-id>` with actual IDs before execution. Also replace 42 and 58 in the manual example with notes that exist in your library.



```bash
curl   --request   POST   \  
    'http://localhost:3721/api/notes/<note-id>/discover-relations'  
  curl   'http://localhost:3721/api/notes/<note-id>/relations'
```



This is a synchronous discovery request; a model failure is returned as an error. It is not a background guarantee. To create a relation manually, provide a target ID, enum type, and description:



```bash
curl   --request   POST   http://localhost:3721/api/notes/42/relations   \  
    --header   'Content-Type: application/json'   \  
    --data   '{"target_note_id":58,"relation_type":"RESOLVED_BY","description":"Verify against the test result"}'  
  curl   --request   DELETE   'http://localhost:3721/api/relations/<relation-id>'
```



The route validates both notes, rejects self-relations and unknown types, and returns HTTP 400 for a manual duplicate.

## Expand search while retaining the source

Semantic search accepts `expand=true` to add one-hop relations. It expands only edges with confidence at least `0.5`, and linked scores use `direct score × 0.7 × confidence`. Notes already in the result set are skipped. This is not multihop path search and does not choose the strongest path for every linked result. Read the note and original conversation rather than treating `via_relation` as validation.

The batch-discovery endpoint enqueues notes with no outgoing relation. Its `queued` value counts submissions; it does not promise that every relation will be generated and should not replace per-note checks.

## Make the graph reviewable

Record source, target, type, confidence, and origin, then test a concrete claim: can both notes’ summaries or source conversations support this edge? Preserve counterexamples and check current project conditions. A tag co-occurrence edge only says that two tags occur in the same notes; it does not establish causality.

See the [semantic search workflow](/en/blog/semantic-search-workflow) for retrieval and projection boundaries, and the [Agent knowledge loop](/en/blog/agent-knowledge-memory-loop) for writeback quality gates.

Share:

##  Related Posts

[##  Graph-Assisted Refactoring: Turning Relational Clues into Checks

Turn note and code graph relations into verifiable fault checks: trace candidate edges, check dependencies, and avoid treating links as proof.          曾亮懿       October 7, 2026      5 min read

#ChatCrystal

#Knowledge graph

#Testing](/en/blog/graph-assisted-refactoring)    [##  Semantic Search and Context: Finding Hidden Code Dependencies

Combine vector search with one-hop graph relation expansion in code refactoring, keeping similarity scores, candidates, and sources verifiable on disk.          曾亮懿       October 3, 2026      3 min read

#ChatCrystal

#Semantic search

#Knowledge graph](/en/blog/semantic-search-relation-context)    [##  AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Note Relations and Graph Views: Discover, Project, and Verify",
  "description": "Trace ChatCrystal relation APIs, candidate discovery, and graph projections, keeping generated edges and tag co-occurrence within verified boundaries.",
  "url": "https://zengliangyi.com/en/blog/note-relations-and-graph/",
  "image": "https://zengliangyi.com/images/blog/note-relations-and-graph.svg",
  "datePublished": "2026-10-03T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/note-relations-and-graph/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Note Relations and Graph Views: Discover, Project, and Verify",
      "item": "https://zengliangyi.com/en/blog/note-relations-and-graph/"
    }
  ]
}
```
