Skip to content

Commit 10fe6d0

Browse files
committed
readme
1 parent c9b0602 commit 10fe6d0

File tree

14 files changed

+427
-102
lines changed

14 files changed

+427
-102
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ on:
99
env:
1010
DOTNET_VERSION: '10.0.x'
1111
SOLUTION_FILE: GraphRag.slnx
12-
GRAPH_RAG_ENABLE_JANUS: 'true'
1312
GRAPH_RAG_ENABLE_COSMOS: 'false'
1413

1514
jobs:

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ on:
88
env:
99
DOTNET_VERSION: '10.0.x'
1010
SOLUTION_FILE: GraphRag.slnx
11-
GRAPH_RAG_ENABLE_JANUS: 'true'
1211
GRAPH_RAG_ENABLE_COSMOS: 'false'
1312

1413
jobs:

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- All integration tests must run against real dependencies via Testcontainers modules (Neo4j, Postgres/AGE, Cosmos, Janus, etc.); do not fall back to Aspire seeding or mock containers.
2121
- Keep integration tests provider-agnostic; avoid adding Postgres-only scenarios when the same flow should apply to every backend.
2222
- Disable Cosmos DB integration tests by default; only enable the emulator when explicitly requested (set `GRAPH_RAG_ENABLE_COSMOS=true`).
23+
- Always enable JanusGraph integration tests: start the Janus Testcontainers module just like Neo4j/Postgres so Janus coverage runs in every `dotnet test` execution.
2324

2425
# Conversations
2526
any resulting updates to agents.md should go under the section "## Rules to follow"

README.md

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,24 @@ GraphRAG ships with a first-class Apache AGE adapter (`ManagedCode.GraphRag.Post
309309

310310
The `AgeConnectionManager` automatically retries transient `53300: too many clients` errors (up to three exponential backoff attempts) so scopes can wait for a free slot before failing. When a scope is disposed, the underlying `IAgeClientScope` created by `IAgeClientFactory` returns its connection to the pool, keeping concurrency predictable even under heavy fan-out.
311311

312+
Need to tune pooling or other Npgsql settings? Set `options.ConfigureConnectionStringBuilder` / `ConfigureDataSourceBuilder` when registering the store:
313+
314+
```csharp
315+
builder.Services.AddPostgresGraphStore("postgres", options =>
316+
{
317+
options.ConnectionString = postgresConnectionString;
318+
options.ConfigureConnectionStringBuilder = builder =>
319+
{
320+
builder.MaxPoolSize = 80;
321+
builder.MinPoolSize = 40;
322+
};
323+
options.ConfigureDataSourceBuilder = ds =>
324+
{
325+
ds.EnableArrayNullabilityMode();
326+
};
327+
});
328+
```
329+
312330
### Neo4j Setup
313331

314332
Neo4j support lives in `ManagedCode.GraphRag.Neo4j` and uses the official Bolt driver:
@@ -332,22 +350,60 @@ Neo4j support lives in `ManagedCode.GraphRag.Neo4j` and uses the official Bolt d
332350
```
333351
The first Neo4j registration will automatically satisfy `IGraphStore`; use `GetRequiredKeyedService<IGraphStore>("neo4j")` for explicit access.
334352

353+
You can also override the auth token and driver config:
354+
355+
```csharp
356+
builder.Services.AddNeo4jGraphStore("neo4j", options =>
357+
{
358+
options.Uri = "neo4j+s://example.databases.neo4j.io";
359+
options.AuthTokenFactory = _ => AuthTokens.Basic("user", "pass");
360+
options.ConfigureDriver = config => config.WithMaxConnectionPoolSize(50);
361+
// Or bypass everything and provide your own driver:
362+
options.DriverFactory = opts => GraphDatabase.Driver(opts.Uri, AuthTokens.None);
363+
});
364+
```
365+
366+
### JanusGraph Setup
367+
368+
JanusGraph support (`ManagedCode.GraphRag.JanusGraph`) uses Gremlin.Net under the hood and now starts automatically in the integration fixture. Register it just like the other stores:
369+
370+
```csharp
371+
builder.Services.AddJanusGraphStore("janus", options =>
372+
{
373+
options.Host = "localhost";
374+
options.Port = 8182;
375+
options.ConnectionPoolSize = 16; // optional
376+
options.MaxInProcessPerConnection = 32; // optional
377+
options.ConfigureConnectionPool = pool =>
378+
{
379+
pool.MaxInProcessPerConnection = Math.Max(pool.MaxInProcessPerConnection, 8);
380+
};
381+
});
382+
```
383+
384+
By default the adapter uses a 32-connection pool with 64 in-flight requests per connection, but you can override those numbers (or mutate the underlying `ConnectionPoolSettings` directly) via the new option properties shown above.
385+
335386
### Azure Cosmos DB Setup
336387

337388
The Cosmos adapter (`ManagedCode.GraphRag.CosmosDb`) targets the SQL API and works with the emulator or live accounts:
338389

339390
1. **Provide a connection string.** Set `COSMOS_EMULATOR_CONNECTION_STRING` or configure options manually.
340391
2. **Register the store.**
341392
```csharp
342-
builder.Services.AddCosmosGraphStore("cosmos", options =>
343-
{
344-
options.ConnectionString = cosmosConnectionString;
345-
options.DatabaseId = "GraphRagIntegration";
346-
options.NodesContainerId = "nodes";
347-
options.EdgesContainerId = "edges";
348-
});
349-
```
350-
As with other adapters, the first Cosmos store becomes the unkeyed default.
393+
builder.Services.AddCosmosGraphStore("cosmos", options =>
394+
{
395+
options.ConnectionString = cosmosConnectionString;
396+
options.DatabaseId = "GraphRagIntegration";
397+
options.NodesContainerId = "nodes";
398+
options.EdgesContainerId = "edges";
399+
options.ConfigureClientOptions = clientOptions =>
400+
{
401+
clientOptions.GatewayModeMaxConnectionLimit = 100;
402+
};
403+
options.ConfigureSerializer = serializer => serializer.PropertyNamingPolicy = null;
404+
});
405+
```
406+
As with other adapters, the first Cosmos store becomes the unkeyed default. If you already have a `CosmosClient`, set `options.ClientFactory` to return it and GraphRAG will reuse that instance.
351407

352408
> **Tip:** `IGraphStore` now exposes full graph inspection and mutation helpers (`GetNodesAsync`, `GetRelationshipsAsync`, `DeleteNodesAsync`, `DeleteRelationshipsAsync`) in addition to the targeted APIs (`InitializeAsync`, `Upsert*`, `GetOutgoingRelationshipsAsync`). These use the same AGE-powered primitives, so you can inspect, prune, or export the graph without dropping down to concrete implementations.
353409

src/ManagedCode.GraphRag.CosmosDb/ServiceCollectionExtensions.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@ public static IServiceCollection AddCosmosGraphStore(this IServiceCollection ser
1919
configure(options);
2020

2121
services.AddKeyedSingleton<CosmosGraphStoreOptions>(key, (_, _) => options);
22-
services.AddKeyedSingleton<CosmosClient>(key, (_, _) =>
22+
services.AddKeyedSingleton<CosmosClient>(key, (sp, serviceKey) =>
2323
{
24+
var opts = sp.GetRequiredKeyedService<CosmosGraphStoreOptions>(serviceKey);
2425
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
2526
{
2627
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
2728
};
29+
opts.ConfigureSerializer?.Invoke(serializerOptions);
30+
2831
var cosmosOptions = new CosmosClientOptions
2932
{
3033
Serializer = new SystemTextJsonCosmosSerializer(serializerOptions)
3134
};
35+
opts.ConfigureClientOptions?.Invoke(cosmosOptions);
3236

33-
return new CosmosClient(options.ConnectionString, cosmosOptions);
37+
return opts.ClientFactory?.Invoke(cosmosOptions) ?? new CosmosClient(opts.ConnectionString, cosmosOptions);
3438
});
3539
services.AddKeyedSingleton<CosmosGraphStore>(key, (sp, serviceKey) =>
3640
{
@@ -57,4 +61,10 @@ public sealed class CosmosGraphStoreOptions
5761
public string NodesContainerId { get; set; } = "nodes";
5862

5963
public string EdgesContainerId { get; set; } = "edges";
64+
65+
public Action<JsonSerializerOptions>? ConfigureSerializer { get; set; }
66+
67+
public Action<CosmosClientOptions>? ConfigureClientOptions { get; set; }
68+
69+
public Func<CosmosClientOptions, CosmosClient>? ClientFactory { get; set; }
6070
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Gremlin.Net.Structure.IO.GraphSON;
2+
3+
namespace GraphRag.Storage.JanusGraph;
4+
5+
internal static class JanusGraphGraphSONSerializerFactory
6+
{
7+
private static readonly IReadOnlyDictionary<string, IGraphSONDeserializer> CustomDeserializers = new Dictionary<string, IGraphSONDeserializer>(StringComparer.Ordinal)
8+
{
9+
["janusgraph:RelationIdentifier"] = new JanusGraphRelationIdentifierDeserializer()
10+
};
11+
12+
public static GraphSON3MessageSerializer Create() =>
13+
new(new GraphSON3Reader(CustomDeserializers), new GraphSON3Writer());
14+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Text.Json;
2+
using Gremlin.Net.Structure.IO.GraphSON;
3+
4+
namespace GraphRag.Storage.JanusGraph;
5+
6+
internal sealed class JanusGraphRelationIdentifierDeserializer : IGraphSONDeserializer
7+
{
8+
public dynamic Objectify(JsonElement graphsonObject, GraphSONReader _)
9+
{
10+
if (graphsonObject.ValueKind != JsonValueKind.Object)
11+
{
12+
return Normalize(graphsonObject) ?? string.Empty;
13+
}
14+
15+
if (graphsonObject.TryGetProperty("relationId", out var relationId) &&
16+
TryGetString(relationId, out var identifier))
17+
{
18+
return identifier ?? string.Empty;
19+
}
20+
21+
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
22+
foreach (var property in graphsonObject.EnumerateObject())
23+
{
24+
result[property.Name] = Normalize(property.Value);
25+
}
26+
27+
return result;
28+
}
29+
30+
private static bool TryGetString(JsonElement element, out string? value)
31+
{
32+
switch (element.ValueKind)
33+
{
34+
case JsonValueKind.String:
35+
value = element.GetString();
36+
return true;
37+
case JsonValueKind.Object when element.TryGetProperty("@value", out var nested):
38+
return TryGetString(nested, out value);
39+
default:
40+
value = null;
41+
return false;
42+
}
43+
}
44+
45+
private static object? Normalize(JsonElement element)
46+
{
47+
return element.ValueKind switch
48+
{
49+
JsonValueKind.Object when element.TryGetProperty("@value", out var inner) => Normalize(inner),
50+
JsonValueKind.Object => NormalizeObject(element),
51+
JsonValueKind.Array => NormalizeArray(element),
52+
JsonValueKind.String => element.GetString(),
53+
JsonValueKind.Number => element.TryGetInt64(out var i64) ? i64 : element.GetDouble(),
54+
JsonValueKind.True => true,
55+
JsonValueKind.False => false,
56+
JsonValueKind.Null => null,
57+
_ => element.GetRawText()
58+
};
59+
}
60+
61+
private static object?[] NormalizeArray(JsonElement element)
62+
{
63+
var items = new List<object?>(element.GetArrayLength());
64+
foreach (var child in element.EnumerateArray())
65+
{
66+
items.Add(Normalize(child));
67+
}
68+
69+
return items.ToArray();
70+
}
71+
72+
private static IDictionary<string, object?> NormalizeObject(JsonElement element)
73+
{
74+
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
75+
foreach (var property in element.EnumerateObject())
76+
{
77+
result[property.Name] = Normalize(property.Value);
78+
}
79+
80+
return result;
81+
}
82+
}

0 commit comments

Comments
 (0)