|
1 | 1 | # Foundatio.LuceneQueryParser |
| 2 | + |
| 3 | +A high-performance Lucene query string parser for .NET that converts query strings into an Abstract Syntax Tree (AST). Supports query transformation via visitors and includes Entity Framework Core integration for generating LINQ expressions. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Full Lucene Query Syntax** - Terms, phrases, fields, ranges, boolean operators, wildcards, regex, and more |
| 8 | +- **Elasticsearch Extensions** - Date math expressions (`now-1d`, `2024-01-01||+1M/d`), `_exists_`, `_missing_` |
| 9 | +- **Visitor Pattern** - Transform, validate, or analyze queries with composable visitors |
| 10 | +- **Round-Trip Capable** - Parse queries to AST and convert back to query strings |
| 11 | +- **Entity Framework Integration** - Convert Lucene queries directly to LINQ expressions |
| 12 | +- **Error Recovery** - Resilient parser returns partial AST with detailed error information |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +```bash |
| 17 | +# Core parser |
| 18 | +dotnet add package Foundatio.LuceneQueryParser |
| 19 | + |
| 20 | +# Entity Framework integration (optional) |
| 21 | +dotnet add package Foundatio.LuceneQueryParser.EntityFramework |
| 22 | +``` |
| 23 | + |
| 24 | +## Quick Start |
| 25 | + |
| 26 | +### Basic Parsing |
| 27 | + |
| 28 | +```csharp |
| 29 | +using Foundatio.LuceneQueryParser; |
| 30 | + |
| 31 | +var result = LuceneQuery.Parse("title:hello AND status:active"); |
| 32 | + |
| 33 | +if (result.IsSuccess) |
| 34 | +{ |
| 35 | + var document = result.Document; // QueryDocument (root AST node) |
| 36 | +} |
| 37 | +else |
| 38 | +{ |
| 39 | + // Handle errors - partial AST may still be available |
| 40 | + foreach (var error in result.Errors) |
| 41 | + Console.WriteLine($"Error at {error.Line}:{error.Column}: {error.Message}"); |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +### Convert AST Back to Query String |
| 46 | + |
| 47 | +```csharp |
| 48 | +using Foundatio.LuceneQueryParser; |
| 49 | + |
| 50 | +var result = LuceneQuery.Parse("title:test AND (status:active OR status:pending)"); |
| 51 | +var queryString = QueryStringBuilder.ToQueryString(result.Document); |
| 52 | +// Returns: "title:test AND (status:active OR status:pending)" |
| 53 | +``` |
| 54 | + |
| 55 | +### Field Aliasing |
| 56 | + |
| 57 | +```csharp |
| 58 | +using Foundatio.LuceneQueryParser; |
| 59 | +using Foundatio.LuceneQueryParser.Visitors; |
| 60 | + |
| 61 | +var result = LuceneQuery.Parse("user:john AND created:[2020-01-01 TO 2020-12-31]"); |
| 62 | + |
| 63 | +var fieldMap = new FieldMap |
| 64 | +{ |
| 65 | + { "user", "account.username" }, |
| 66 | + { "created", "metadata.timestamp" } |
| 67 | +}; |
| 68 | + |
| 69 | +await FieldResolverQueryVisitor.RunAsync(result.Document, fieldMap); |
| 70 | + |
| 71 | +var resolved = QueryStringBuilder.ToQueryString(result.Document); |
| 72 | +// Returns: "account.username:john AND metadata.timestamp:[2020-01-01 TO 2020-12-31]" |
| 73 | +``` |
| 74 | + |
| 75 | +### Query Validation |
| 76 | + |
| 77 | +```csharp |
| 78 | +using Foundatio.LuceneQueryParser; |
| 79 | + |
| 80 | +var result = LuceneQuery.Parse("*wildcard AND title:test"); |
| 81 | + |
| 82 | +var options = new QueryValidationOptions |
| 83 | +{ |
| 84 | + AllowLeadingWildcards = false |
| 85 | +}; |
| 86 | +options.AllowedFields.Add("title"); |
| 87 | +options.AllowedFields.Add("status"); |
| 88 | + |
| 89 | +var validationResult = await QueryValidator.ValidateAsync(result.Document, options); |
| 90 | + |
| 91 | +if (!validationResult.IsValid) |
| 92 | + Console.WriteLine(validationResult.Message); |
| 93 | +``` |
| 94 | + |
| 95 | +### Entity Framework Integration |
| 96 | + |
| 97 | +```csharp |
| 98 | +using Foundatio.LuceneQueryParser.EntityFramework; |
| 99 | + |
| 100 | +var parser = new EntityFrameworkQueryParser(); |
| 101 | + |
| 102 | +// Build a filter expression from a Lucene query |
| 103 | +Expression<Func<Employee, bool>> filter = parser.BuildFilter<Employee>( |
| 104 | + "name:john AND salary:[50000 TO *] AND isActive:true" |
| 105 | +); |
| 106 | + |
| 107 | +// Use with EF Core |
| 108 | +var results = await context.Employees.Where(filter).ToListAsync(); |
| 109 | +``` |
| 110 | + |
| 111 | +## Supported Query Syntax |
| 112 | + |
| 113 | +| Syntax | Example | Description | |
| 114 | +|--------|---------|-------------| |
| 115 | +| Terms | `hello`, `hello*`, `hel?o` | Simple terms with optional wildcards | |
| 116 | +| Phrases | `"hello world"`, `"hello world"~2` | Exact phrases with optional proximity | |
| 117 | +| Fields | `title:test`, `user.name:john` | Field-specific queries, supports nested paths | |
| 118 | +| Ranges | `price:[100 TO 500]`, `date:{* TO 2024-01-01}` | Inclusive `[]` or exclusive `{}` ranges | |
| 119 | +| Boolean | `AND`, `OR`, `NOT`, `+`, `-` | Boolean operators and prefix modifiers | |
| 120 | +| Groups | `(a OR b) AND c` | Parenthetical grouping | |
| 121 | +| Exists | `_exists_:field`, `_missing_:field` | Field existence checks | |
| 122 | +| Match All | `*:*` | Matches all documents | |
| 123 | +| Regex | `/pattern/` | Regular expression patterns | |
| 124 | +| Date Math | `now-1d`, `2024-01-01\|\|+1M/d` | Elasticsearch date math expressions | |
| 125 | +| Includes | `@include:savedQuery` | Reference saved/named queries | |
| 126 | + |
| 127 | +## Creating Custom Visitors |
| 128 | + |
| 129 | +Extend `QueryNodeVisitor` to create custom transformations: |
| 130 | + |
| 131 | +```csharp |
| 132 | +using Foundatio.LuceneQueryParser.Ast; |
| 133 | +using Foundatio.LuceneQueryParser.Visitors; |
| 134 | + |
| 135 | +public class LowercaseTermVisitor : QueryNodeVisitor |
| 136 | +{ |
| 137 | + public override Task<QueryNode> VisitAsync(TermNode node, IQueryVisitorContext context) |
| 138 | + { |
| 139 | + node.Term = node.Term?.ToLowerInvariant(); |
| 140 | + return Task.FromResult<QueryNode>(node); |
| 141 | + } |
| 142 | + |
| 143 | + public override async Task<QueryNode> VisitAsync(FieldQueryNode node, IQueryVisitorContext context) |
| 144 | + { |
| 145 | + // Process this node's field |
| 146 | + node.Field = node.Field?.ToLowerInvariant(); |
| 147 | + |
| 148 | + // Visit children |
| 149 | + return await base.VisitAsync(node, context); |
| 150 | + } |
| 151 | +} |
| 152 | + |
| 153 | +// Usage |
| 154 | +var visitor = new LowercaseTermVisitor(); |
| 155 | +await visitor.RunAsync(result.Document); |
| 156 | +``` |
| 157 | + |
| 158 | +### Chaining Multiple Visitors |
| 159 | + |
| 160 | +```csharp |
| 161 | +var chain = new ChainedQueryVisitor() |
| 162 | + .AddVisitor(new FieldAliasVisitor(aliases), priority: 10) |
| 163 | + .AddVisitor(new LowercaseTermVisitor(), priority: 20) |
| 164 | + .AddVisitor(new ValidationVisitor(), priority: 30); |
| 165 | + |
| 166 | +await chain.AcceptAsync(document, context); |
| 167 | +``` |
| 168 | + |
| 169 | +## AST Node Types |
| 170 | + |
| 171 | +| Node Type | Description | |
| 172 | +|-----------|-------------| |
| 173 | +| `QueryDocument` | Root node containing the parsed query | |
| 174 | +| `TermNode` | Simple term (e.g., `hello`) | |
| 175 | +| `PhraseNode` | Quoted phrase (e.g., `"hello world"`) | |
| 176 | +| `FieldQueryNode` | Field:value pair (e.g., `title:test`) | |
| 177 | +| `RangeNode` | Range query (e.g., `[1 TO 10]`) | |
| 178 | +| `BooleanQueryNode` | Boolean combination of clauses | |
| 179 | +| `GroupNode` | Parenthetical group | |
| 180 | +| `NotNode` | Negation wrapper | |
| 181 | +| `ExistsNode` | `_exists_:field` check | |
| 182 | +| `MissingNode` | `_missing_:field` check | |
| 183 | +| `MatchAllNode` | `*:*` match all | |
| 184 | +| `RegexNode` | Regular expression | |
| 185 | +| `MultiTermNode` | Multiple terms without explicit operators | |
| 186 | + |
| 187 | +## Building |
| 188 | + |
| 189 | +```bash |
| 190 | +dotnet build |
| 191 | +dotnet test |
| 192 | +``` |
| 193 | + |
| 194 | +## License |
| 195 | + |
| 196 | +Apache 2.0 |
0 commit comments