Skip to content

Commit d6e44cf

Browse files
Copilotjongalloway
andcommitted
Complete multi-backend implementation with documentation and integration tests
Co-authored-by: jongalloway <[email protected]>
1 parent 8893a4c commit d6e44cf

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

doc/multi-backend-configuration.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Multi-Backend Configuration Example
2+
3+
This example demonstrates how to configure and use the multi-backend retrieval architecture in NLWebNet.
4+
5+
## Single Backend (Default/Legacy)
6+
7+
```csharp
8+
// Traditional single backend setup - still works
9+
services.AddNLWebNet<MockDataBackend>(options =>
10+
{
11+
options.DefaultMode = QueryMode.List;
12+
options.MaxResultsPerQuery = 20;
13+
});
14+
```
15+
16+
## Multi-Backend Configuration
17+
18+
```csharp
19+
// New multi-backend setup
20+
services.AddNLWebNetMultiBackend(
21+
options =>
22+
{
23+
options.DefaultMode = QueryMode.List;
24+
options.MaxResultsPerQuery = 50;
25+
},
26+
multiBackendOptions =>
27+
{
28+
multiBackendOptions.Enabled = true;
29+
multiBackendOptions.EnableParallelQuerying = true;
30+
multiBackendOptions.EnableResultDeduplication = true;
31+
multiBackendOptions.MaxConcurrentQueries = 3;
32+
multiBackendOptions.BackendTimeoutSeconds = 30;
33+
multiBackendOptions.WriteEndpoint = "primary_backend";
34+
});
35+
```
36+
37+
## Configuration via appsettings.json
38+
39+
```json
40+
{
41+
"NLWebNet": {
42+
"DefaultMode": "List",
43+
"MaxResultsPerQuery": 50,
44+
"MultiBackend": {
45+
"Enabled": true,
46+
"EnableParallelQuerying": true,
47+
"EnableResultDeduplication": true,
48+
"MaxConcurrentQueries": 3,
49+
"BackendTimeoutSeconds": 30,
50+
"WriteEndpoint": "primary_backend",
51+
"Endpoints": {
52+
"primary_backend": {
53+
"Enabled": true,
54+
"BackendType": "azure_ai_search",
55+
"Priority": 10,
56+
"Properties": {
57+
"ConnectionString": "your-connection-string",
58+
"IndexName": "your-index"
59+
}
60+
},
61+
"secondary_backend": {
62+
"Enabled": true,
63+
"BackendType": "mock",
64+
"Priority": 5,
65+
"Properties": {}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
```
72+
73+
## Usage Example
74+
75+
```csharp
76+
public class ExampleController : ControllerBase
77+
{
78+
private readonly INLWebService _nlWebService;
79+
private readonly IBackendManager _backendManager;
80+
81+
public ExampleController(INLWebService nlWebService, IBackendManager backendManager)
82+
{
83+
_nlWebService = nlWebService;
84+
_backendManager = backendManager;
85+
}
86+
87+
[HttpPost("search")]
88+
public async Task<IActionResult> Search([FromBody] NLWebRequest request)
89+
{
90+
// Multi-backend search automatically handled
91+
var response = await _nlWebService.ProcessRequestAsync(request);
92+
return Ok(response);
93+
}
94+
95+
[HttpGet("backend-info")]
96+
public IActionResult GetBackendInfo()
97+
{
98+
// Get information about configured backends
99+
var backendInfo = _backendManager.GetBackendInfo();
100+
return Ok(backendInfo);
101+
}
102+
103+
[HttpGet("write-backend-capabilities")]
104+
public IActionResult GetWriteBackendCapabilities()
105+
{
106+
// Access the designated write backend
107+
var writeBackend = _backendManager.GetWriteBackend();
108+
if (writeBackend == null)
109+
{
110+
return NotFound("No write backend configured");
111+
}
112+
113+
var capabilities = writeBackend.GetCapabilities();
114+
return Ok(capabilities);
115+
}
116+
}
117+
```
118+
119+
## Key Features
120+
121+
### Parallel Querying
122+
- Queries execute simultaneously across all enabled backends
123+
- Configurable concurrency limits and timeouts
124+
- Graceful handling of backend failures
125+
126+
### Result Deduplication
127+
- Automatic deduplication based on URL
128+
- Higher scoring results from different backends take precedence
129+
- Can be disabled for scenarios requiring all results
130+
131+
### Write Endpoint
132+
- Designate one backend as the primary write endpoint
133+
- Other backends remain read-only for queries
134+
- Useful for hybrid architectures
135+
136+
### Backward Compatibility
137+
- Existing single-backend configurations continue to work
138+
- No breaking changes to existing APIs
139+
- Gradual migration path available
140+
141+
## Migration from Single Backend
142+
143+
1. Replace `AddNLWebNet<T>()` with `AddNLWebNetMultiBackend()`
144+
2. Set `MultiBackend.Enabled = false` initially to maintain existing behavior
145+
3. Configure additional backends in the `Endpoints` section
146+
4. Enable multi-backend mode by setting `MultiBackend.Enabled = true`
147+
5. Test and adjust concurrency and timeout settings as needed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
using NLWebNet.Models;
5+
using NLWebNet.Services;
6+
7+
namespace NLWebNet.Tests.Integration;
8+
9+
[TestClass]
10+
public class MultiBackendIntegrationTests
11+
{
12+
[TestMethod]
13+
public async Task EndToEnd_MultiBackendSearch_WorksCorrectly()
14+
{
15+
// Arrange - Set up a complete multi-backend service configuration
16+
var services = new ServiceCollection();
17+
18+
services.AddNLWebNetMultiBackend(
19+
options =>
20+
{
21+
options.DefaultMode = QueryMode.List;
22+
options.MaxResultsPerQuery = 20;
23+
options.EnableDecontextualization = false; // Simplify for test
24+
},
25+
multiBackendOptions =>
26+
{
27+
multiBackendOptions.Enabled = true;
28+
multiBackendOptions.EnableParallelQuerying = true;
29+
multiBackendOptions.EnableResultDeduplication = true;
30+
multiBackendOptions.MaxConcurrentQueries = 2;
31+
multiBackendOptions.BackendTimeoutSeconds = 10;
32+
});
33+
34+
var serviceProvider = services.BuildServiceProvider();
35+
var nlWebService = serviceProvider.GetRequiredService<INLWebService>();
36+
var backendManager = serviceProvider.GetRequiredService<IBackendManager>();
37+
38+
// Act - Perform a search using the NLWebService
39+
var request = new NLWebRequest
40+
{
41+
QueryId = "test-001",
42+
Query = "millennium falcon",
43+
Mode = QueryMode.List,
44+
Site = null
45+
};
46+
47+
var response = await nlWebService.ProcessRequestAsync(request);
48+
49+
// Assert - Verify the response contains results from multiple backends
50+
Assert.IsNotNull(response);
51+
Assert.AreEqual("test-001", response.QueryId);
52+
Assert.IsNull(response.Error, "Response should not have an error");
53+
Assert.IsNotNull(response.Results);
54+
Assert.IsTrue(response.Results.Any(), "Should return search results");
55+
56+
// Verify backend manager provides information about backends
57+
var backendInfo = backendManager.GetBackendInfo().ToList();
58+
Assert.IsTrue(backendInfo.Count >= 1, "Should have at least one backend configured");
59+
Assert.IsTrue(backendInfo.Any(b => b.IsWriteEndpoint), "Should have a write endpoint designated");
60+
61+
// Verify write backend is accessible
62+
var writeBackend = backendManager.GetWriteBackend();
63+
Assert.IsNotNull(writeBackend, "Should have a write backend available");
64+
65+
var capabilities = writeBackend.GetCapabilities();
66+
Assert.IsNotNull(capabilities, "Write backend should have capabilities");
67+
}
68+
69+
[TestMethod]
70+
public async Task EndToEnd_MultiBackendDisabled_FallsBackToSingleBackend()
71+
{
72+
// Arrange - Set up multi-backend service but with multi-backend disabled
73+
var services = new ServiceCollection();
74+
75+
services.AddNLWebNetMultiBackend(
76+
options =>
77+
{
78+
options.DefaultMode = QueryMode.List;
79+
options.MaxResultsPerQuery = 20;
80+
options.EnableDecontextualization = false;
81+
options.MultiBackend.Enabled = false; // Disabled for backward compatibility
82+
});
83+
84+
var serviceProvider = services.BuildServiceProvider();
85+
var nlWebService = serviceProvider.GetRequiredService<INLWebService>();
86+
87+
// Act - Perform a search
88+
var request = new NLWebRequest
89+
{
90+
QueryId = "test-002",
91+
Query = "millennium falcon",
92+
Mode = QueryMode.List
93+
};
94+
95+
var response = await nlWebService.ProcessRequestAsync(request);
96+
97+
// Assert - Should still work in single-backend mode
98+
Assert.IsNotNull(response);
99+
Assert.AreEqual("test-002", response.QueryId);
100+
Assert.IsNull(response.Error, "Response should not have an error");
101+
102+
// Verify configuration
103+
var options = serviceProvider.GetRequiredService<IOptions<NLWebOptions>>();
104+
Assert.IsFalse(options.Value.MultiBackend.Enabled, "Multi-backend should be disabled");
105+
}
106+
107+
[TestMethod]
108+
public async Task EndToEnd_StreamingResponse_WorksWithMultiBackend()
109+
{
110+
// Arrange
111+
var services = new ServiceCollection();
112+
113+
services.AddNLWebNetMultiBackend(options =>
114+
{
115+
options.EnableStreaming = true;
116+
options.MultiBackend.Enabled = true;
117+
});
118+
119+
var serviceProvider = services.BuildServiceProvider();
120+
var nlWebService = serviceProvider.GetRequiredService<INLWebService>();
121+
122+
// Act - Test streaming response
123+
var request = new NLWebRequest
124+
{
125+
QueryId = "test-003",
126+
Query = "millennium falcon",
127+
Mode = QueryMode.List,
128+
Streaming = true
129+
};
130+
131+
var responseCount = 0;
132+
await foreach (var response in nlWebService.ProcessRequestStreamAsync(request))
133+
{
134+
responseCount++;
135+
Assert.IsNotNull(response);
136+
Assert.AreEqual("test-003", response.QueryId);
137+
138+
// Break after a few responses to avoid long test
139+
if (responseCount >= 3) break;
140+
}
141+
142+
Assert.IsTrue(responseCount > 0, "Should receive streaming responses");
143+
}
144+
145+
[TestMethod]
146+
public async Task EndToEnd_DeduplicationAcrossBackends_WorksCorrectly()
147+
{
148+
// Arrange
149+
var services = new ServiceCollection();
150+
151+
services.AddNLWebNetMultiBackend(options =>
152+
{
153+
options.MultiBackend.Enabled = true;
154+
options.MultiBackend.EnableResultDeduplication = true;
155+
});
156+
157+
var serviceProvider = services.BuildServiceProvider();
158+
var backendManager = serviceProvider.GetRequiredService<IBackendManager>();
159+
160+
// Act - Direct test of backend manager deduplication
161+
var results = await backendManager.SearchAsync("millennium falcon", maxResults: 20);
162+
163+
// Assert
164+
var resultList = results.ToList();
165+
var uniqueUrls = resultList.Select(r => r.Url).Distinct().Count();
166+
167+
Assert.AreEqual(resultList.Count, uniqueUrls,
168+
"Results should be deduplicated - no duplicate URLs");
169+
170+
if (resultList.Count > 1)
171+
{
172+
// Verify results are sorted by score
173+
var scores = resultList.Select(r => r.Score).ToList();
174+
var sortedScores = scores.OrderByDescending(s => s).ToList();
175+
CollectionAssert.AreEqual(sortedScores, scores,
176+
"Results should be sorted by relevance score");
177+
}
178+
}
179+
}

0 commit comments

Comments
 (0)