# Common Cypher Patterns for ORLY Nostr Relay This reference contains project-specific Cypher patterns used in the ORLY Nostr relay's Neo4j backend. ## Schema Overview ### Node Types | Label | Purpose | Key Properties | |-------|---------|----------------| | `Event` | Nostr events (NIP-01) | `id`, `kind`, `pubkey`, `created_at`, `content`, `sig`, `tags`, `serial` | | `Author` | Event authors (for NIP-01 queries) | `pubkey` | | `Tag` | Generic tags | `type`, `value` | | `NostrUser` | Social graph users (WoT) | `pubkey`, `name`, `about`, `picture`, `nip05` | | `ProcessedSocialEvent` | Social event tracking | `event_id`, `event_kind`, `pubkey`, `superseded_by` | | `Marker` | Internal state markers | `key`, `value` | ### Relationship Types | Type | From | To | Purpose | |------|------|-----|---------| | `AUTHORED_BY` | Event | Author | Links event to author | | `TAGGED_WITH` | Event | Tag | Links event to tags | | `REFERENCES` | Event | Event | e-tag references | | `MENTIONS` | Event | Author | p-tag mentions | | `FOLLOWS` | NostrUser | NostrUser | Contact list (kind 3) | | `MUTES` | NostrUser | NostrUser | Mute list (kind 10000) | | `REPORTS` | NostrUser | NostrUser | Reports (kind 1984) | ## Event Storage Patterns ### Create Event with Full Relationships This pattern creates an event and all related nodes/relationships atomically: ```cypher // 1. Create or get author MERGE (a:Author {pubkey: $pubkey}) // 2. Create event node CREATE (e:Event { id: $eventId, serial: $serial, kind: $kind, created_at: $createdAt, content: $content, sig: $sig, pubkey: $pubkey, tags: $tagsJson // JSON string for full tag data }) // 3. Link to author CREATE (e)-[:AUTHORED_BY]->(a) // 4. Process e-tags (event references) WITH e, a OPTIONAL MATCH (ref0:Event {id: $eTag_0}) FOREACH (_ IN CASE WHEN ref0 IS NOT NULL THEN [1] ELSE [] END | CREATE (e)-[:REFERENCES]->(ref0) ) // 5. Process p-tags (mentions) WITH e, a MERGE (mentioned0:Author {pubkey: $pTag_0}) CREATE (e)-[:MENTIONS]->(mentioned0) // 6. Process other tags WITH e, a MERGE (tag0:Tag {type: $tagType_0, value: $tagValue_0}) CREATE (e)-[:TAGGED_WITH]->(tag0) RETURN e.id AS id ``` ### Check Event Existence ```cypher MATCH (e:Event {id: $id}) RETURN e.id AS id LIMIT 1 ``` ### Get Next Serial Number ```cypher MERGE (m:Marker {key: 'serial'}) ON CREATE SET m.value = 1 ON MATCH SET m.value = m.value + 1 RETURN m.value AS serial ``` ## Query Patterns ### Basic Filter Query (NIP-01) ```cypher MATCH (e:Event) WHERE e.kind IN $kinds AND e.pubkey IN $authors AND e.created_at >= $since AND e.created_at <= $until RETURN e.id AS id, e.kind AS kind, e.created_at AS created_at, e.content AS content, e.sig AS sig, e.pubkey AS pubkey, e.tags AS tags, e.serial AS serial ORDER BY e.created_at DESC LIMIT $limit ``` ### Query by Event ID (with prefix support) ```cypher // Exact match MATCH (e:Event {id: $id}) RETURN e // Prefix match MATCH (e:Event) WHERE e.id STARTS WITH $idPrefix RETURN e ``` ### Query by Tag (# filter) ```cypher MATCH (e:Event) OPTIONAL MATCH (e)-[:TAGGED_WITH]->(t:Tag) WHERE t.type = $tagType AND t.value IN $tagValues RETURN DISTINCT e ORDER BY e.created_at DESC LIMIT $limit ``` ### Count Events ```cypher MATCH (e:Event) WHERE e.kind IN $kinds RETURN count(e) AS count ``` ### Query Delete Events Targeting an Event ```cypher MATCH (target:Event {id: $targetId}) MATCH (e:Event {kind: 5})-[:REFERENCES]->(target) RETURN e ORDER BY e.created_at DESC ``` ### Replaceable Event Check (kinds 0, 3, 10000-19999) ```cypher MATCH (e:Event {kind: $kind, pubkey: $pubkey}) WHERE e.created_at < $newCreatedAt RETURN e.serial AS serial ORDER BY e.created_at DESC ``` ### Parameterized Replaceable Event Check (kinds 30000-39999) ```cypher MATCH (e:Event {kind: $kind, pubkey: $pubkey})-[:TAGGED_WITH]->(t:Tag {type: 'd', value: $dValue}) WHERE e.created_at < $newCreatedAt RETURN e.serial AS serial ORDER BY e.created_at DESC ``` ## Social Graph Patterns ### Update Profile (Kind 0) ```cypher MERGE (user:NostrUser {pubkey: $pubkey}) ON CREATE SET user.created_at = timestamp(), user.first_seen_event = $event_id ON MATCH SET user.last_profile_update = $created_at SET user.name = $name, user.about = $about, user.picture = $picture, user.nip05 = $nip05, user.lud16 = $lud16, user.display_name = $display_name ``` ### Contact List Update (Kind 3) - Diff-Based ```cypher // Mark old event as superseded OPTIONAL MATCH (old:ProcessedSocialEvent {event_id: $old_event_id}) SET old.superseded_by = $new_event_id // Create new event tracking CREATE (new:ProcessedSocialEvent { event_id: $new_event_id, event_kind: 3, pubkey: $author_pubkey, created_at: $created_at, processed_at: timestamp(), relationship_count: $total_follows, superseded_by: null }) // Get or create author MERGE (author:NostrUser {pubkey: $author_pubkey}) // Update unchanged relationships to new event WITH author OPTIONAL MATCH (author)-[unchanged:FOLLOWS]->(followed:NostrUser) WHERE unchanged.created_by_event = $old_event_id AND NOT followed.pubkey IN $removed_follows SET unchanged.created_by_event = $new_event_id, unchanged.created_at = $created_at // Remove old relationships for removed follows WITH author OPTIONAL MATCH (author)-[old_follows:FOLLOWS]->(followed:NostrUser) WHERE old_follows.created_by_event = $old_event_id AND followed.pubkey IN $removed_follows DELETE old_follows // Create new relationships for added follows WITH author UNWIND $added_follows AS followed_pubkey MERGE (followed:NostrUser {pubkey: followed_pubkey}) MERGE (author)-[new_follows:FOLLOWS]->(followed) ON CREATE SET new_follows.created_by_event = $new_event_id, new_follows.created_at = $created_at, new_follows.relay_received_at = timestamp() ON MATCH SET new_follows.created_by_event = $new_event_id, new_follows.created_at = $created_at ``` ### Create Report (Kind 1984) ```cypher // Create tracking node CREATE (evt:ProcessedSocialEvent { event_id: $event_id, event_kind: 1984, pubkey: $reporter_pubkey, created_at: $created_at, processed_at: timestamp(), relationship_count: 1, superseded_by: null }) // Create users and relationship MERGE (reporter:NostrUser {pubkey: $reporter_pubkey}) MERGE (reported:NostrUser {pubkey: $reported_pubkey}) CREATE (reporter)-[:REPORTS { created_by_event: $event_id, created_at: $created_at, relay_received_at: timestamp(), report_type: $report_type }]->(reported) ``` ### Get Latest Social Event for Pubkey ```cypher MATCH (evt:ProcessedSocialEvent {pubkey: $pubkey, event_kind: $kind}) WHERE evt.superseded_by IS NULL RETURN evt.event_id AS event_id, evt.created_at AS created_at, evt.relationship_count AS relationship_count ORDER BY evt.created_at DESC LIMIT 1 ``` ### Get Follows for Event ```cypher MATCH (author:NostrUser)-[f:FOLLOWS]->(followed:NostrUser) WHERE f.created_by_event = $event_id RETURN collect(followed.pubkey) AS pubkeys ``` ## WoT Query Patterns ### Find Mutual Follows ```cypher MATCH (a:NostrUser {pubkey: $pubkeyA})-[:FOLLOWS]->(b:NostrUser) WHERE (b)-[:FOLLOWS]->(a) RETURN b.pubkey AS mutual_friend ``` ### Find Followers ```cypher MATCH (follower:NostrUser)-[:FOLLOWS]->(user:NostrUser {pubkey: $pubkey}) RETURN follower.pubkey, follower.name ``` ### Find Following ```cypher MATCH (user:NostrUser {pubkey: $pubkey})-[:FOLLOWS]->(following:NostrUser) RETURN following.pubkey, following.name ``` ### Hop Distance (Trust Path) ```cypher MATCH (start:NostrUser {pubkey: $startPubkey}) MATCH (end:NostrUser {pubkey: $endPubkey}) MATCH path = shortestPath((start)-[:FOLLOWS*..6]->(end)) RETURN length(path) AS hops, [n IN nodes(path) | n.pubkey] AS path ``` ### Second-Degree Connections ```cypher MATCH (me:NostrUser {pubkey: $myPubkey})-[:FOLLOWS]->(:NostrUser)-[:FOLLOWS]->(suggested:NostrUser) WHERE NOT (me)-[:FOLLOWS]->(suggested) AND suggested.pubkey <> $myPubkey RETURN suggested.pubkey, count(*) AS commonFollows ORDER BY commonFollows DESC LIMIT 20 ``` ## Schema Management Patterns ### Create Constraint ```cypher CREATE CONSTRAINT event_id_unique IF NOT EXISTS FOR (e:Event) REQUIRE e.id IS UNIQUE ``` ### Create Index ```cypher CREATE INDEX event_kind IF NOT EXISTS FOR (e:Event) ON (e.kind) ``` ### Create Composite Index ```cypher CREATE INDEX event_kind_created_at IF NOT EXISTS FOR (e:Event) ON (e.kind, e.created_at) ``` ### Drop All Data (Testing Only) ```cypher MATCH (n) DETACH DELETE n ``` ## Performance Patterns ### Use EXPLAIN/PROFILE ```cypher // See query plan without running EXPLAIN MATCH (e:Event) WHERE e.kind = 1 RETURN e // Run and see actual metrics PROFILE MATCH (e:Event) WHERE e.kind = 1 RETURN e ``` ### Batch Import with UNWIND ```cypher UNWIND $events AS evt CREATE (e:Event { id: evt.id, kind: evt.kind, pubkey: evt.pubkey, created_at: evt.created_at, content: evt.content, sig: evt.sig, tags: evt.tags }) ``` ### Efficient Pagination ```cypher // Use indexed ORDER BY with WHERE for cursor-based pagination MATCH (e:Event) WHERE e.kind = 1 AND e.created_at < $cursor RETURN e ORDER BY e.created_at DESC LIMIT 20 ```