diff --git a/AzureSearchEmulator.IntegrationTests/EmptyIndexIntegrationTests.cs b/AzureSearchEmulator.IntegrationTests/EmptyIndexIntegrationTests.cs
new file mode 100644
index 0000000..49dc890
--- /dev/null
+++ b/AzureSearchEmulator.IntegrationTests/EmptyIndexIntegrationTests.cs
@@ -0,0 +1,42 @@
+using Azure.Search.Documents.Indexes.Models;
+using Azure.Search.Documents.Models;
+using Xunit;
+
+namespace AzureSearchEmulator.IntegrationTests;
+
+///
+/// An index that exists but has never had a document written answers queries with nothing,
+/// as in Azure, instead of failing because its Lucene directory has not been created yet.
+///
+public class EmptyIndexIntegrationTests(EmulatorFactory factory)
+ : IClassFixture
+{
+ [Fact]
+ public async Task NeverWrittenIndex_SearchesAndGetsReturnNothing()
+ {
+ var indexName = "empty-" + Guid.NewGuid().ToString("N")[..8];
+ var cancellationToken = TestContext.Current.CancellationToken;
+
+ await factory.CreateSearchIndexClient().CreateIndexAsync(
+ new SearchIndex(indexName)
+ {
+ Fields =
+ {
+ new SearchField("id", SearchFieldDataType.String) { IsKey = true },
+ new SearchField("name", SearchFieldDataType.String) { IsSearchable = true },
+ }
+ },
+ cancellationToken);
+
+ var client = factory.CreateSearchClient(indexName);
+
+ var results = await client.SearchAsync("x", new Azure.Search.Documents.SearchOptions { IncludeTotalCount = true }, cancellationToken);
+ Assert.Equal(0, results.Value.TotalCount);
+
+ Assert.Equal(0, (await client.GetDocumentCountAsync(cancellationToken)).Value);
+
+ var ex = await Assert.ThrowsAsync(
+ async () => await client.GetDocumentAsync("1", cancellationToken: cancellationToken));
+ Assert.Equal(404, ex.Status);
+ }
+}
diff --git a/AzureSearchEmulator.IntegrationTests/ErrorResponseIntegrationTests.cs b/AzureSearchEmulator.IntegrationTests/ErrorResponseIntegrationTests.cs
index ba49908..7293d58 100644
--- a/AzureSearchEmulator.IntegrationTests/ErrorResponseIntegrationTests.cs
+++ b/AzureSearchEmulator.IntegrationTests/ErrorResponseIntegrationTests.cs
@@ -176,50 +176,6 @@ public async Task DocumentLookupWithUnknownSelectField_IsRejectedAsBadRequest()
await indexClient.DeleteIndexAsync(indexName, TestContext.Current.CancellationToken);
}
- ///
- /// An unexpected fault still answers in the envelope, and without leaking internals.
- ///
- ///
- /// Searching an index that has never had a document written to it currently trips a Lucene
- /// DirectoryNotFoundException, because the segment directory is only created on first write.
- /// That 500 is arguably its own bug — the real service answers an empty result set — so this
- /// test deliberately asserts only on the SHAPE of an unexpected failure, not on this query
- /// being one. Should the empty-index case be fixed to return results, the assertions below
- /// stop applying and the test should be pointed at another unexpected fault rather than
- /// being made to keep this 500 alive.
- ///
- /// The generic message is the point: the underlying exception names the emulator's own
- /// filesystem paths, and those belong in the log rather than the response body.
- ///
- [Fact]
- public async Task UnexpectedFailure_ReturnsEnvelopeWithoutInternalDetail()
- {
- const string indexName = $"{IndexName}-unexpected";
- var indexClient = factory.CreateSearchIndexClient();
- await indexClient.CreateIndexAsync(BuildIndex(indexName), TestContext.Current.CancellationToken);
-
- var http = factory.CreateHttpClient();
- var response = await http.GetAsync(
- $"/indexes/{indexName}/docs?search=*",
- TestContext.Current.CancellationToken);
-
- var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-
- // Guard the premise: if this stops being a server fault, the assertions below are no
- // longer testing what they claim to.
- Assert.True(response.StatusCode == HttpStatusCode.InternalServerError,
- $"This test needs a query that faults unexpectedly; got {(int)response.StatusCode}. Body: {body}");
-
- using var document = JsonDocument.Parse(body);
- var message = document.RootElement.GetProperty("error").GetProperty("message").GetString();
-
- Assert.Equal("An unexpected error occurred.", message);
- Assert.DoesNotContain("/app", body, StringComparison.Ordinal);
- Assert.DoesNotContain("Exception", body, StringComparison.Ordinal);
-
- await indexClient.DeleteIndexAsync(indexName, TestContext.Current.CancellationToken);
- }
-
///
/// What the consumer sees: the SDK reads the message out of the envelope instead of
/// falling back to its own "Service request failed."
diff --git a/AzureSearchEmulator/Searching/LuceneNetIndexSearcher.cs b/AzureSearchEmulator/Searching/LuceneNetIndexSearcher.cs
index 2d049f5..a5704ff 100644
--- a/AzureSearchEmulator/Searching/LuceneNetIndexSearcher.cs
+++ b/AzureSearchEmulator/Searching/LuceneNetIndexSearcher.cs
@@ -1097,10 +1097,30 @@ private static JsonObject ConvertSearchDoc(
}
}
- private IndexSearcher GetSearcher(SearchIndex index)
+ ///
+ /// Stands in for the index directory of an index that has never had a document written: the
+ /// directory is created on first write, so until then there is nothing to open, and Azure
+ /// answers such an index with an empty result rather than an error.
+ ///
+ private static readonly Lazy EmptyReader = new(() =>
{
- var reader = indexReaderFactory.GetIndexReader(index.Name);
+ var directory = new Lucene.Net.Store.RAMDirectory();
+ var config = new IndexWriterConfig(Lucene.Net.Util.LuceneVersion.LUCENE_48, new Lucene.Net.Analysis.Core.KeywordAnalyzer());
+
+ new IndexWriter(directory, config).Dispose();
- return new IndexSearcher(reader);
+ return DirectoryReader.Open(directory);
+ });
+
+ private IndexSearcher GetSearcher(SearchIndex index)
+ {
+ try
+ {
+ return new IndexSearcher(indexReaderFactory.GetIndexReader(index.Name));
+ }
+ catch (DirectoryNotFoundException)
+ {
+ return new IndexSearcher(EmptyReader.Value);
+ }
}
}