Field Aliases
Field aliases allow you to map user-friendly field names to actual field paths in your data. This is useful for:
- Hiding complex nested field paths from users
- Providing backward compatibility when field names change
- Creating domain-specific query languages
- Abstracting internal data structures
Static Field Maps
The simplest approach is a static dictionary mapping aliases to actual field names:
using Foundatio.Parsers.LuceneQueries;
using Foundatio.Parsers.LuceneQueries.Visitors;
var parser = new LuceneQueryParser();
var result = await parser.ParseAsync("user:john");
// Define field mappings
var fieldMap = new FieldMap {
{ "user", "data.user.identity" },
{ "created", "metadata.createdAt" },
{ "status", "workflow.currentStatus" }
};
// Apply field resolution
var resolved = await FieldResolverQueryVisitor.RunAsync(result, fieldMap);
// Result: data.user.identity:john
Console.WriteLine(resolved.ToString());With ElasticQueryParser
using Foundatio.Parsers.ElasticQueries;
var parser = new ElasticQueryParser(c => c
.UseFieldMap(new Dictionary<string, string> {
{ "user", "data.user.identity" },
{ "created", "metadata.createdAt" },
{ "status", "workflow.currentStatus" }
}));
// Query uses aliases
var query = await parser.BuildQueryAsync("user:john AND status:active");
// Internally resolves to: data.user.identity:john AND workflow.currentStatus:activeHierarchical Field Resolution
For nested field structures, use hierarchical resolution. This resolves parent paths and preserves child segments:
using Foundatio.Parsers.LuceneQueries.Visitors;
var fieldMap = new Dictionary<string, string> {
{ "user", "data.profile.user" },
{ "user.address", "data.profile.user.location" }
};
// Convert to hierarchical resolver
var resolver = fieldMap.ToHierarchicalFieldResolver();
// Resolution examples:
// "user" -> "data.profile.user"
// "user.name" -> "data.profile.user.name" (parent resolved, child preserved)
// "user.address" -> "data.profile.user.location" (exact match)
// "user.address.city" -> "data.profile.user.location.city" (parent resolved)How Hierarchical Resolution Works
- Check for exact match in the map
- If not found, split the field by
.and check for parent matches - Replace the matched parent with its mapping, preserve remaining segments
var map = new Dictionary<string, string> {
{ "original", "replacement" },
{ "original.nested", "otherreplacement" }
};
var resolver = map.ToHierarchicalFieldResolver();
// Examples:
await resolver("notmapped", null); // "notmapped" (no change)
await resolver("original", null); // "replacement"
await resolver("original.hey", null); // "replacement.hey"
await resolver("original.nested", null); // "otherreplacement"
await resolver("original.nested.hey", null); // "otherreplacement.hey"Dynamic Field Resolvers
For complex resolution logic, use a custom resolver function:
using Foundatio.Parsers.LuceneQueries.Visitors;
var parser = new ElasticQueryParser(c => c
.UseFieldResolver(async (field, context) => {
// Custom resolution logic
if (field.StartsWith("custom."))
{
return field.Replace("custom.", "data.custom_fields.");
}
// Check a database or external service
var mapping = await GetFieldMappingFromDatabase(field);
if (mapping != null)
return mapping;
// Return null to keep original field name
return null;
}));Resolver Signature
public delegate Task<string> QueryFieldResolver(string field, IQueryVisitorContext context);The resolver receives:
field- The field name to resolvecontext- The visitor context with additional data
Return:
- The resolved field name, or
nullto keep the original field name
Combining Static and Dynamic Resolution
You can combine static maps with dynamic resolution:
var staticMap = new Dictionary<string, string> {
{ "user", "data.user.identity" },
{ "status", "workflow.status" }
};
var parser = new ElasticQueryParser(c => c
.UseFieldResolver(async (field, context) => {
// First check static map
if (staticMap.TryGetValue(field, out var mapped))
return mapped;
// Then apply dynamic logic
if (field.StartsWith("meta."))
return field.Replace("meta.", "metadata.");
// Check hierarchical resolution for nested fields
var hierarchical = staticMap.ToHierarchicalFieldResolver();
return await hierarchical(field, context);
}));Field Resolution in Groups
Field aliases work with grouped queries:
var parser = new LuceneQueryParser();
var result = await parser.ParseAsync("(user.name:john OR user.email:john@example.com) status:active");
var fieldMap = new FieldMap {
{ "user.name", "profile.fullName" },
{ "user.email", "profile.emailAddress" },
{ "status", "account.status" }
};
var resolved = await FieldResolverQueryVisitor.RunAsync(result, fieldMap);
// Result: (profile.fullName:john OR profile.emailAddress:john@example.com) account.status:activeField Resolution with Aggregations
Field aliases apply to aggregations as well:
var parser = new ElasticQueryParser(c => c
.UseMappings(client, "my-index")
.UseFieldMap(new Dictionary<string, string> {
{ "category", "product.category.keyword" },
{ "price", "product.pricing.amount" }
}));
// Aggregation uses aliases
var aggs = await parser.BuildAggregationsAsync("terms:(category min:price max:price)");
// Resolves to: terms:(product.category.keyword min:product.pricing.amount max:product.pricing.amount)Preserving Original Field Names
The original field name is preserved in node metadata for reference:
var parser = new LuceneQueryParser();
var result = await parser.ParseAsync("user:john");
var fieldMap = new FieldMap { { "user", "data.user.identity" } };
var context = new QueryVisitorContext();
await FieldResolverQueryVisitor.RunAsync(result, fieldMap, context);
// Access original field name
var termNode = result.Left as TermNode;
string original = termNode.GetOriginalField(); // "user"
string resolved = termNode.Field; // "data.user.identity"Validation with Field Aliases
When using validation, specify allowed fields using the alias names (not resolved names):
var parser = new ElasticQueryParser(c => c
.UseFieldMap(new Dictionary<string, string> {
{ "user", "data.user.identity" },
{ "status", "workflow.status" }
})
.SetValidationOptions(new QueryValidationOptions {
// Use alias names in allowed fields
AllowedFields = { "user", "status", "created" }
}));
// Valid - uses allowed alias
var result = await parser.ValidateQueryAsync("user:john");
// result.IsValid == true
// Invalid - uses resolved name directly
result = await parser.ValidateQueryAsync("data.user.identity:john");
// result.IsValid == false (unless also in AllowedFields)Best Practices
1. Use Descriptive Alias Names
// Good - clear, domain-specific names
var fieldMap = new Dictionary<string, string> {
{ "author", "document.metadata.author.name" },
{ "published", "document.metadata.publishedDate" }
};
// Avoid - cryptic abbreviations
var fieldMap = new Dictionary<string, string> {
{ "a", "document.metadata.author.name" },
{ "pd", "document.metadata.publishedDate" }
};2. Document Your Aliases
Maintain documentation of available aliases for API consumers:
/// <summary>
/// Available field aliases for the search API:
/// - user: Maps to data.user.identity
/// - status: Maps to workflow.currentStatus
/// - created: Maps to metadata.createdAt
/// </summary>3. Handle Unknown Fields Gracefully
var parser = new ElasticQueryParser(c => c
.UseFieldResolver(async (field, context) => {
if (knownAliases.TryGetValue(field, out var mapped))
return mapped;
// Option 1: Return null to keep original (permissive)
return null;
// Option 2: Use validation to reject unknown fields (strict)
// Configure AllowedFields in validation options
})
.SetValidationOptions(new QueryValidationOptions {
AllowUnresolvedFields = false // Reject unknown fields
}));4. Consider Case Sensitivity
var parser = new ElasticQueryParser(c => c
.UseFieldResolver(async (field, context) => {
// Case-insensitive lookup
var key = fieldMap.Keys.FirstOrDefault(k =>
k.Equals(field, StringComparison.OrdinalIgnoreCase));
return key != null ? fieldMap[key] : null;
}));Next Steps
- Query Includes - Reusable query macros
- Validation - Validate and restrict queries
- Elasticsearch Integration - Full parser configuration