Configuration
This guide covers configuration options for the various components of Foundatio.Lucene.
Core Parser
The core parser is stateless and requires no configuration:
var result = LuceneQuery.Parse("title:hello AND status:active");LuceneQuery.Parse is resilient: it always returns a (possibly partial) AST plus any ParseErrors, and bounds nesting depth (LuceneParser.MaxDepth, default 100) so deeply nested input can never overflow the stack.
Entity Framework Parser
var parser = new EntityFrameworkQueryParser(config =>
{
config.SetDefaultFields("Name", "Email");
config.SetDefaultOperator(BooleanOperator.And);
// Controls 'now' in date math (now-7d, now/d). Defaults to TimeProvider.System.
config.SetTimeProvider(TimeProvider.System);
});
var filter = parser.BuildFilter<Employee>("name:john AND salary:[50000 TO *]");
var employees = context.Employees.Where(filter).ToList();Field aliasing, includes and date math (per request)
Field maps, @include references and date math are applied through the shared visitor pipeline, exactly as in the Elasticsearch integration. Supply them per request via EntityFrameworkQueryOptions (the recommended way to swap configuration per scope/tenant):
var options = new EntityFrameworkQueryOptionsBuilder()
.WithFieldMap(new FieldMap { { "user", "Name" }, { "dept", "Department.Name" } })
.WithIncludes(new Dictionary<string, string> { ["seniors"] = "Title:Senior" })
.WithValidationOptions(o => o.AllowLeadingWildcards = false)
.Build();
var filter = parser.BuildFilter<Employee>("user:john AND @include:seniors", context: null, options);You can also register default options per entity type with parser.SetOptions<Employee>(options). Per-request options take precedence over registered options, which take precedence over the global configuration.
SQL cannot honor fuzzy (
term~N) or proximity/slop ("a b"~N) queries. Rather than silently returning the wrong rows, the EF builder throwsQueryBuildException(useTryBuildFilterto get aQueryResultinstead of an exception). Regex queries depend on the database provider.
Elasticsearch Parser
var parser = new ElasticsearchQueryParser(config =>
{
config.UseScoring = true; // match queries (scoring) vs term queries (filter)
config.DefaultFields = ["title", "content"]; // fields for unfielded terms
config.DefaultOperator = BooleanOperator.And; // implicit operator
config.FieldMap = new FieldMap { { "author", "metadata.author" } };
config.IsDateField = field => field.EndsWith("At") || field.EndsWith("date");
config.DefaultTimeZone = "America/Chicago";
config.ValidationOptions = new QueryValidationOptions { AllowLeadingWildcards = false };
});
var query = parser.BuildQuery("author:john AND status:active");Configuration properties
| Property | Type | Default | Description |
|---|---|---|---|
UseScoring | bool | false | Use match queries (scoring) vs term queries (filtering) |
DefaultFields | string[]? | null | Fields to search for unfielded terms |
DefaultOperator | BooleanOperator | Or | Default boolean operator for implicit combinations |
FieldMap | FieldMap? | null | Field name mappings |
Includes | IReadOnlyDictionary<string,string>? | null | Pre-resolved @include content |
IsDateField | Func<string, bool>? | null | Detects date fields for date range queries |
DefaultTimeZone | string? | null | Default timezone for date range queries |
ValidationOptions | QueryValidationOptions? | null | Query validation options |
TimeProvider | TimeProvider | System | Controls now in date math |
Per-request / per-scope options
Swap configuration per scope by passing a prebuilt ElasticsearchQueryOptions (or registering it per index name). Options are immutable records, so an application can cache one per tenant and reuse it across requests:
var tenantOptions = new ElasticsearchQueryOptionsBuilder()
.WithFieldMap(new FieldMap { { "user", "tenant42.userName" } })
.WithDefaultFields("tenant42.title")
.Build();
var query = parser.BuildQuery("user:john", tenantOptions);The same parser instance is thread-safe and reuses its visitors across requests, so swapping options per request does not allocate a visitor per call.
Field Map
var fieldMap = new FieldMap
{
{ "user", "account.username" },
{ "created", "metadata.createdAt" }
};FieldMap is case-insensitive. By default ResolutionMode is Hierarchical, so nested paths resolve by longest matching prefix:
var fieldMap = new FieldMap
{
{ "data", "payload" },
{ "data.user", "payload.account.username" }
};
// "data.user:john" -> "payload.account.username:john"
// "data.status:open" -> "payload.status:open"Set ResolutionMode = FieldResolutionMode.Direct for exact-match-only resolution, ReportUnmappedFields = true to flag unmapped fields as unresolved, or ResultPrefix to prefix every resolved name. The parsers apply the field map automatically; to run it manually:
FieldResolverQueryVisitor.Run(result.Document, fieldMap);Includes
@include:name references expand from a pre-resolved dictionary (resolve saved queries from your store before parsing):
var includes = new Dictionary<string, string> { ["recent"] = "created:[now-7d TO now]" };
var parser = new ElasticsearchQueryParser(c => c.Includes = includes);
var query = parser.BuildQuery("@include:recent AND status:active");
// Or run it manually against the AST:
result.Document.ExpandIncludes(includes);Circular references and excessive nesting (IncludeVisitor.MaxIncludeDepth) are detected and reported as validation errors.
Validation
var options = new QueryValidationOptions
{
AllowLeadingWildcards = false,
AllowWildcardOnlyQueries = false,
AllowedFields = { "title", "author", "status", "date" },
DisallowedFields = { "password", "ssn" }
};
var result = QueryValidator.ValidateQuery("title:hello", options);
if (!result.IsValid)
{
// result.ValidationErrors describe what failed
}| Property | Type | Default | Description |
|---|---|---|---|
AllowLeadingWildcards | bool | true | Allow patterns like *suffix |
AllowWildcardOnlyQueries | bool | true | Allow * or *:* queries |
AllowedFields | HashSet<string> | empty | Whitelist of allowed fields |
DisallowedFields | HashSet<string> | empty | Blacklist of disallowed fields |
Custom Visitors
Compose visitors with ChainedQueryVisitor and run them synchronously:
var visitors = new ChainedQueryVisitor()
.AddVisitor(new FieldResolverQueryVisitor(fieldMap), priority: 10)
.AddVisitor(new DateMathEvaluatorVisitor(), priority: 30)
.AddVisitor(new ValidationVisitor(), priority: 100);
var context = new QueryVisitorContext();
visitors.Accept(result.Document, context);Dependency Injection
services.AddSingleton<EntityFrameworkQueryParser>();
services.AddSingleton(sp => new ElasticsearchQueryParser(config =>
{
config.UseScoring = true;
config.DefaultFields = ["title", "content"];
config.ValidationOptions = new QueryValidationOptions { AllowLeadingWildcards = false };
}));Next Steps
- Getting Started - Quick start guide
- Elasticsearch - Elasticsearch integration
- Validation - Query validation