Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
* Creates an index on CodeNode.id for fast MATCH during edge creation.
* Logs progress every 10K items for visibility on large graphs.
*/
public void bulkSave(List<CodeNode> nodes) {

Check warning on line 71 in src/main/java/io/github/randomcodespace/iq/graph/GraphStore.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 82 to 64, Complexity from 19 to 14, Nesting Level from 3 to 2, Number of Variables from 29 to 6.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ1J0yEr4NxuHIEx6uMV&open=AZ1J0yEr4NxuHIEx6uMV&pullRequest=12
if (nodes.isEmpty()) return;
long start = System.currentTimeMillis();

Expand Down Expand Up @@ -273,6 +273,31 @@
Map.of("nodeId", nodeId));
}

/**
* Batch-find all ENDPOINT/WEBSOCKET_ENDPOINT neighbors for a list of node IDs in one query.
* Returns a map of sourceNodeId -> list of endpoint neighbor nodes.
*/
public Map<String, List<CodeNode>> findEndpointNeighborsBatch(List<String> nodeIds) {
Map<String, List<CodeNode>> result = new java.util.LinkedHashMap<>();
if (nodeIds.isEmpty()) return result;
try (Transaction tx = graphDb.beginTx()) {
var queryResult = tx.execute(
"MATCH (n:CodeNode)-[]-(m:CodeNode) "
+ "WHERE n.id IN $nodeIds AND m.kind IN ['ENDPOINT', 'WEBSOCKET_ENDPOINT'] "
+ "RETURN n.id AS sourceId, m",
Map.of("nodeIds", nodeIds));
while (queryResult.hasNext()) {
var row = queryResult.next();
String sourceId = (String) row.get("sourceId");
Object val = row.get("m");
if (val instanceof org.neo4j.graphdb.Node neo4jNode) {
result.computeIfAbsent(sourceId, k -> new ArrayList<>()).add(nodeFromNeo4j(neo4jNode));
}
}
}
return result;
}

public long count() {
try (Transaction tx = graphDb.beginTx()) {
var result = tx.execute("MATCH (n:CodeNode) RETURN count(n) AS cnt");
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/io/github/randomcodespace/iq/query/QueryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -337,19 +337,24 @@ public Map<String, Object> findRelatedEndpoints(String identifier) {
Set<String> seenIds = new java.util.LinkedHashSet<>();
List<Map<String, Object>> endpoints = new ArrayList<>();

// First pass: collect matches that are themselves endpoints
for (CodeNode match : matches) {
if (match.getKind() == NodeKind.ENDPOINT || match.getKind() == NodeKind.WEBSOCKET_ENDPOINT) {
if (seenIds.add(match.getId())) {
endpoints.add(nodeToMap(match));
}
}
// Check neighbors for connected endpoints
List<CodeNode> neighbors = graphStore.findNeighbors(match.getId());
for (CodeNode neighbor : neighbors) {
if ((neighbor.getKind() == NodeKind.ENDPOINT || neighbor.getKind() == NodeKind.WEBSOCKET_ENDPOINT)
&& seenIds.add(neighbor.getId())) {
}

// Single batched query for all endpoint neighbors (replaces N+1 loop)
List<String> matchIds = matches.stream().map(CodeNode::getId).toList();
Map<String, List<CodeNode>> endpointNeighbors = graphStore.findEndpointNeighborsBatch(matchIds);
for (Map.Entry<String, List<CodeNode>> entry : endpointNeighbors.entrySet()) {
String sourceId = entry.getKey();
for (CodeNode neighbor : entry.getValue()) {
if (seenIds.add(neighbor.getId())) {
Map<String, Object> epMap = nodeToMap(neighbor);
epMap.put("connected_via", match.getId());
epMap.put("connected_via", sourceId);
endpoints.add(epMap);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,59 @@ void findDeadCodeShouldReturnEmptyWhenAllNodesHaveSemanticEdges() {
assertTrue(deadCode.isEmpty());
}

// --- findRelatedEndpoints ---

@Test
void findRelatedEndpointsShouldUsesBatchQueryInsteadOfNPlusOne() {
var classNode = makeNode("cls:UserService", NodeKind.CLASS, "UserService");
var endpointNode = makeNode("ep:getUsers", NodeKind.ENDPOINT, "getUsers");
when(graphStore.search("UserService", 50)).thenReturn(List.of(classNode));
when(graphStore.findEndpointNeighborsBatch(List.of("cls:UserService")))
.thenReturn(Map.of("cls:UserService", List.of(endpointNode)));

Map<String, Object> result = service.findRelatedEndpoints("UserService");

assertEquals("UserService", result.get("identifier"));
assertEquals(1, result.get("count"));
assertEquals(1, result.get("searched_nodes"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> endpoints = (List<Map<String, Object>>) result.get("endpoints");
assertEquals("ep:getUsers", endpoints.getFirst().get("id"));
assertEquals("cls:UserService", endpoints.getFirst().get("connected_via"));
// Verify no per-node findNeighbors calls were made
verify(graphStore, never()).findNeighbors(anyString());
}

@Test
void findRelatedEndpointsShouldIncludeDirectEndpointMatches() {
var endpointNode = makeNode("ep:getUsers", NodeKind.ENDPOINT, "getUsers");
when(graphStore.search("getUsers", 50)).thenReturn(List.of(endpointNode));
when(graphStore.findEndpointNeighborsBatch(List.of("ep:getUsers"))).thenReturn(Map.of());

Map<String, Object> result = service.findRelatedEndpoints("getUsers");

assertEquals(1, result.get("count"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> endpoints = (List<Map<String, Object>>) result.get("endpoints");
assertEquals("ep:getUsers", endpoints.getFirst().get("id"));
// Direct endpoint matches have no connected_via
assertNull(endpoints.getFirst().get("connected_via"));
}

@Test
void findRelatedEndpointsShouldDeduplicateEndpoints() {
var endpointNode = makeNode("ep:getUsers", NodeKind.ENDPOINT, "getUsers");
// Same endpoint appears as both a direct match and a neighbor
when(graphStore.search("ep", 50)).thenReturn(List.of(endpointNode));
when(graphStore.findEndpointNeighborsBatch(List.of("ep:getUsers")))
.thenReturn(Map.of("ep:getUsers", List.of(endpointNode)));

Map<String, Object> result = service.findRelatedEndpoints("ep");

// Should only appear once
assertEquals(1, result.get("count"));
}

// --- nodeToMap ---

@Test
Expand Down
Loading