├── .editorconfig
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .idea
└── .idea.Foundatio.Parsers
│ └── .idea
│ ├── encodings.xml
│ ├── indexLayout.xml
│ ├── projectSettingsUpdater.xml
│ └── vcs.xml
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── Foundatio.Parsers.sln
├── LICENSE.txt
├── NuGet.config
├── README.md
├── build
├── Foundatio.snk
├── common.props
└── foundatio-icon.png
├── docker-compose.yml
├── docs
├── aggregations.md
└── query.md
├── global.json
├── src
├── Foundatio.Parsers.ElasticQueries
│ ├── ElasticMappingResolver.cs
│ ├── ElasticQueryParser.cs
│ ├── ElasticQueryParserConfiguration.cs
│ ├── Extensions
│ │ ├── DefaultAggregationNodeExtensions.cs
│ │ ├── DefaultQueryNodeExtensions.cs
│ │ ├── DefaultSortNodeExtensions.cs
│ │ ├── ElasticExtensions.cs
│ │ ├── ElasticMappingExtensions.cs
│ │ ├── QueryNodeExtensions.cs
│ │ ├── QueryVisitorContextExtensions.cs
│ │ └── SearchDescriptorExtensions.cs
│ ├── Foundatio.Parsers.ElasticQueries.csproj
│ └── Visitors
│ │ ├── CombineAggregationsVisitor.cs
│ │ ├── CombineQueriesVisitor.cs
│ │ ├── ElasticQueryVisitorContext.cs
│ │ ├── GeoVisitor.cs
│ │ ├── GetSortFieldsVisitor.cs
│ │ ├── IElasticQueryVisitorContext.cs
│ │ └── NestedVisitor.cs
├── Foundatio.Parsers.LuceneQueries
│ ├── Extensions
│ │ ├── QueryNodeExtensions.cs
│ │ ├── QueryVisitorContextExtensions.cs
│ │ ├── StringExtensions.cs
│ │ └── TextWriterExtensions.cs
│ ├── Foundatio.Parsers.LuceneQueries.csproj
│ ├── LuceneQueryParser.cs
│ ├── LuceneQueryParser.peg
│ ├── Nodes
│ │ ├── ExistsNode.cs
│ │ ├── GroupNode.cs
│ │ ├── IQueryNode.cs
│ │ ├── MissingNode.cs
│ │ ├── QueryNodeBase.cs
│ │ ├── TermNode.cs
│ │ └── TermRangeNode.cs
│ ├── QueryTypes.cs
│ ├── QueryValidation.cs
│ ├── QueryValidator.cs
│ └── Visitors
│ │ ├── AssignOperationTypeVisitor.cs
│ │ ├── ChainedQueryVisitor.cs
│ │ ├── CleanupQueryVisitor.cs
│ │ ├── DebugQueryVisitor.cs
│ │ ├── FieldResolverQueryVisitor.cs
│ │ ├── GenerateQueryVisitor.cs
│ │ ├── GetReferencedFieldsQueryVisitor.cs
│ │ ├── IChainableQueryVisitor.cs
│ │ ├── IQueryNodeVisitor.cs
│ │ ├── IQueryNodeVisitorWithResult.cs
│ │ ├── IQueryVisitorContext.cs
│ │ ├── IQueryVisitorContextWithFieldResolver.cs
│ │ ├── IQueryVisitorContextWithIncludeResolver.cs
│ │ ├── IQueryVisitorContextWithValidation.cs
│ │ ├── IncludeVisitor.cs
│ │ ├── InvertQueryVisitor.cs
│ │ ├── QueryNodeVisitorBase.cs
│ │ ├── QueryNodeVisitorWithResultBase.cs
│ │ ├── QueryVisitorContext.cs
│ │ ├── RemoveFieldsQueryVisitor.cs
│ │ ├── TermToFieldVisitor.cs
│ │ ├── ValidationVisitor.cs
│ │ └── VisitorWithPriority.cs
└── Foundatio.Parsers.SqlQueries
│ ├── Extensions
│ ├── EnumerableExtensions.cs
│ ├── SqlNodeExtensions.cs
│ └── TypeExtensions.cs
│ ├── Foundatio.Parsers.SqlQueries.csproj
│ ├── SqlQueryParser.cs
│ ├── SqlQueryParserConfiguration.cs
│ └── Visitors
│ ├── GenerateSqlVisitor.cs
│ ├── ISqlQueryVisitorContext.cs
│ └── SqlQueryVisitorContext.cs
└── tests
├── Directory.Build.props
├── Foundatio.Parsers.ElasticQueries.Tests
├── AggregationParserTests.cs
├── CustomVisitorTests.cs
├── ElasticMappingResolverTests.cs
├── ElasticQueryParserTests.cs
├── Foundatio.Parsers.ElasticQueries.Tests.csproj
├── InvertQueryTests.cs
└── Utility
│ └── ElasticsearchTestBase.cs
├── Foundatio.Parsers.LuceneQueries.Tests
├── CleanupQueryVisitorTests.cs
├── FieldResolverVisitorTests.cs
├── Foundatio.Parsers.LuceneQueries.Tests.csproj
├── GenerateQueryVisitorTests.cs
├── IncludeQueryVisitorTests.cs
├── InvertQueryVisitorTests.cs
├── QueryParserTests.cs
├── QueryParserValidationTests.cs
├── QueryValidatorTests.cs
├── ReferencedFieldsTests.cs
├── RemoveFieldsQueryVisitor.cs
├── UnescapeTests.cs
└── Utility
│ └── LoggingTracer.cs
└── Foundatio.Parsers.SqlQueries.Tests
├── DynamicFieldVisitor.cs
├── Foundatio.Parsers.SqlQueries.Tests.csproj
├── SampleContext.cs
└── SqlQueryParserTests.cs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: exceptionless
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |
4 | - package-ecosystem: nuget
5 | directory: "/"
6 | schedule:
7 | interval: weekly
8 |
9 | - package-ecosystem: "docker-compose"
10 | directory: "/"
11 | schedule:
12 | interval: quarterly
13 | ignore:
14 | - dependency-name: "elasticsearch/elasticsearch"
15 | versions:
16 | - "<8.0.0"
17 | - ">=9.0.0"
18 | - dependency-name: "kibana/kibana"
19 | versions:
20 | - "<8.0.0"
21 | - ">=9.0.0"
22 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | uses: FoundatioFx/Foundatio/.github/workflows/build-workflow.yml@main
7 | secrets: inherit
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # User-specific files
2 | *.suo
3 | *.user
4 |
5 | # Build results
6 | [Bb]in/
7 | [Oo]bj/
8 | artifacts
9 | .vs/
10 |
11 | # MSTest test Results
12 | [Tt]est[Rr]esult*/
13 |
14 | # ReSharper is a .NET coding add-in
15 | _ReSharper*/
16 | *.[Rr]e[Ss]harper
17 | *.DotSettings.user
18 |
19 | # JustCode is a .NET coding addin-in
20 | .JustCode
21 |
22 | # DotCover is a Code Coverage Tool
23 | *.dotCover
24 |
25 | # NCrunch
26 | _NCrunch_*
27 | .*crunch*.local.xml
28 |
29 | # NuGet Packages
30 | *.nupkg
31 |
32 | .DS_Store
33 |
34 | # Rider
35 |
36 | # User specific
37 | **/.idea/**/workspace.xml
38 | **/.idea/**/tasks.xml
39 | **/.idea/shelf/*
40 | **/.idea/dictionaries
41 |
42 | # Sensitive or high-churn files
43 | **/.idea/**/dataSources/
44 | **/.idea/**/dataSources.ids
45 | **/.idea/**/dataSources.xml
46 | **/.idea/**/dataSources.local.xml
47 | **/.idea/**/sqlDataSources.xml
48 | **/.idea/**/dynamic.xml
49 |
50 | # Rider
51 | # Rider auto-generates .iml files, and contentModel.xml
52 | **/.idea/**/*.iml
53 | **/.idea/**/contentModel.xml
54 | **/.idea/**/modules.xml
55 | **/.idea/copilot/chatSessions/
56 |
--------------------------------------------------------------------------------
/.idea/.idea.Foundatio.Parsers/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/.idea.Foundatio.Parsers/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/.idea.Foundatio.Parsers/.idea/projectSettingsUpdater.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.idea.Foundatio.Parsers/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "theaustinseven.pegs",
4 | "ms-dotnettools.csharp",
5 | "streetsidesoftware.code-spell-checker",
6 | "editorconfig.editorconfig",
7 | "tintoy.msbuild-project-tools"
8 | ]
9 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (console)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/test/Foundatio.Parsers.LuceneQueries.Tests/bin/Debug/net8.0/Foundatio.Parsers.LuceneQueries.Tests.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}/test/Foundatio.Parsers.LuceneQueries.Tests",
16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
17 | "console": "internalConsole",
18 | "stopAtEntry": false,
19 | "internalConsoleOptions": "openOnSessionStart"
20 | },
21 | {
22 | "name": ".NET Core Attach",
23 | "type": "coreclr",
24 | "request": "attach",
25 | "processId": "${command:pickProcess}"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Lucene",
4 | "Niemyjski",
5 | "Xunit",
6 | "aggs"
7 | ],
8 | "msbuildProjectTools.nuget.includePreRelease": true
9 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "group": {
9 | "kind": "build",
10 | "isDefault": true
11 | },
12 | "args": [
13 | "build",
14 | "${workspaceFolder}",
15 | "/p:GenerateFullPaths=true"
16 | ],
17 | "problemMatcher": "$msCompile"
18 | },
19 | {
20 | "label": "test",
21 | "command": "dotnet",
22 | "type": "process",
23 | "group": {
24 | "kind": "test",
25 | "isDefault": true
26 | },
27 | "args": [
28 | "test",
29 | "${workspaceFolder}/tests/Foundatio.Parsers.LuceneQueries.Tests/Foundatio.Parsers.LuceneQueries.Tests.csproj",
30 | "/p:GenerateFullPaths=true"
31 | ],
32 | "problemMatcher": "$msCompile"
33 | },
34 | {
35 | "label": "pack",
36 | "command": "dotnet pack -c Release -o ${workspaceFolder}/artifacts",
37 | "type": "shell",
38 | "problemMatcher": []
39 | },
40 | {
41 | "label": "docker: elasticsearch",
42 | "command": "docker compose up",
43 | "type": "shell",
44 | "isBackground": true,
45 | "group": "test",
46 | "problemMatcher": []
47 | }
48 | ]
49 | }
--------------------------------------------------------------------------------
/Foundatio.Parsers.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30404.54
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.Parsers.LuceneQueries", "src\Foundatio.Parsers.LuceneQueries\Foundatio.Parsers.LuceneQueries.csproj", "{F759E45D-C914-4546-947D-B9F0854D22E9}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.Parsers.ElasticQueries", "src\Foundatio.Parsers.ElasticQueries\Foundatio.Parsers.ElasticQueries.csproj", "{9F6F9CC3-2544-44EB-8829-209187271A4E}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.Parsers.LuceneQueries.Tests", "tests\Foundatio.Parsers.LuceneQueries.Tests\Foundatio.Parsers.LuceneQueries.Tests.csproj", "{DDFFC095-C821-41DA-8DFF-2AD37A56106F}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{400D08AA-50A4-4D74-A3AD-F74AAB8FC706}"
13 | ProjectSection(SolutionItems) = preProject
14 | .editorconfig = .editorconfig
15 | .github\workflows\build.yml = .github\workflows\build.yml
16 | build\common.props = build\common.props
17 | tests\Directory.Build.props = tests\Directory.Build.props
18 | docker-compose.yml = docker-compose.yml
19 | NuGet.config = NuGet.config
20 | README.md = README.md
21 | EndProjectSection
22 | EndProject
23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.Parsers.ElasticQueries.Tests", "tests\Foundatio.Parsers.ElasticQueries.Tests\Foundatio.Parsers.ElasticQueries.Tests.csproj", "{C353E601-874C-4855-9B01-2980BC624BC4}"
24 | EndProject
25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundatio.Parsers.SqlQueries", "src\Foundatio.Parsers.SqlQueries\Foundatio.Parsers.SqlQueries.csproj", "{53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}"
26 | EndProject
27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foundatio.Parsers.SqlQueries.Tests", "tests\Foundatio.Parsers.SqlQueries.Tests\Foundatio.Parsers.SqlQueries.Tests.csproj", "{B30934E7-C69F-465B-BACF-61AAEC7DA775}"
28 | EndProject
29 | Global
30 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
31 | Debug|Any CPU = Debug|Any CPU
32 | Release|Any CPU = Release|Any CPU
33 | EndGlobalSection
34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
35 | {F759E45D-C914-4546-947D-B9F0854D22E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {F759E45D-C914-4546-947D-B9F0854D22E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {F759E45D-C914-4546-947D-B9F0854D22E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
38 | {F759E45D-C914-4546-947D-B9F0854D22E9}.Release|Any CPU.Build.0 = Release|Any CPU
39 | {9F6F9CC3-2544-44EB-8829-209187271A4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {9F6F9CC3-2544-44EB-8829-209187271A4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {9F6F9CC3-2544-44EB-8829-209187271A4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {9F6F9CC3-2544-44EB-8829-209187271A4E}.Release|Any CPU.Build.0 = Release|Any CPU
43 | {DDFFC095-C821-41DA-8DFF-2AD37A56106F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44 | {DDFFC095-C821-41DA-8DFF-2AD37A56106F}.Debug|Any CPU.Build.0 = Debug|Any CPU
45 | {DDFFC095-C821-41DA-8DFF-2AD37A56106F}.Release|Any CPU.ActiveCfg = Release|Any CPU
46 | {DDFFC095-C821-41DA-8DFF-2AD37A56106F}.Release|Any CPU.Build.0 = Release|Any CPU
47 | {C353E601-874C-4855-9B01-2980BC624BC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
48 | {C353E601-874C-4855-9B01-2980BC624BC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
49 | {C353E601-874C-4855-9B01-2980BC624BC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
50 | {C353E601-874C-4855-9B01-2980BC624BC4}.Release|Any CPU.Build.0 = Release|Any CPU
51 | {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52 | {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
53 | {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
54 | {53C0B9D8-6398-437C-AE15-43BD4CB7AD8D}.Release|Any CPU.Build.0 = Release|Any CPU
55 | {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56 | {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Debug|Any CPU.Build.0 = Debug|Any CPU
57 | {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Release|Any CPU.ActiveCfg = Release|Any CPU
58 | {B30934E7-C69F-465B-BACF-61AAEC7DA775}.Release|Any CPU.Build.0 = Release|Any CPU
59 | EndGlobalSection
60 | GlobalSection(SolutionProperties) = preSolution
61 | HideSolutionNode = FALSE
62 | EndGlobalSection
63 | GlobalSection(ExtensibilityGlobals) = postSolution
64 | SolutionGuid = {37B59D3F-7772-4404-9BF3-F2FFE79C47D0}
65 | EndGlobalSection
66 | EndGlobal
67 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://github.com/FoundatioFx/Foundatio.Parsers/actions)
4 | [](https://www.nuget.org/packages/Foundatio.Parsers.LuceneQueries/)
5 | [](https://f.feedz.io/foundatio/foundatio/packages/Foundatio.Parsers.LuceneQueries/latest/download)
6 | [](https://discord.gg/6HxgFCx)
7 |
8 | A lucene style query parser that is extensible and allows additional syntax features. Also includes an Elasticsearch query_string query replacement that greatly enhances its capabilities for dynamic queries.
9 |
10 | ## Getting Started (Development)
11 |
12 | [This package](https://www.nuget.org/packages/Foundatio.Parsers.LuceneQueries/) can be installed via the [NuGet package manager](https://docs.nuget.org/consume/Package-Manager-Dialog). If you need help, please contact us via in-app support or [open an issue](https://github.com/exceptionless/Foundatio.Parsers/issues/new). We’re always here to help if you have any questions!
13 |
14 | 1. You will need to have [Visual Studio Code](https://code.visualstudio.com) installed.
15 | 2. Open the `Foundatio.Parsers.sln` Visual Studio solution file.
16 |
17 | ## Using LuceneQueryParser
18 |
19 | Below is a small sampling of the things you can accomplish with LuceneQueryParser, so check it out! We use this library extensively in [Exceptionless](https://github.com/exceptionless/Exceptionless)!
20 |
21 | In the sample below we will parse a query and output it's structure using the `DebugQueryVisitor` and then generate the same exact query using the parse result.
22 |
23 | ```csharp
24 | using Foundatio.Parsers.LuceneQueries;
25 | using Foundatio.Parsers.LuceneQueries.Visitors;
26 |
27 | var parser = new LuceneQueryParser();
28 | var result = parser.Parse("field:[1 TO 2]");
29 | Debug.WriteLine(DebugQueryVisitor.Run(result));
30 | ```
31 |
32 | Here is the parse result as shown from the `DebugQueryVisitor`
33 | ```
34 | Group:
35 | Left - Term:
36 | TermMax: 2
37 | TermMin: 1
38 | MinInclusive: True
39 | MaxInclusive: True
40 | Field:
41 | Name: field
42 | ```
43 |
44 | Finally, lets translate the parse result back into the original query.
45 | ```csharp
46 | var generatedQuery = GenerateQueryVisitor.Run(result);
47 | System.Diagnostics.Debug.Assert(query == generatedQuery);
48 | ```
49 | ## [Query Syntax](docs/query.md)
50 |
51 | ## [Aggregation Syntax](docs/aggregations.md)
52 |
53 | ## Features
54 | - Lucene Query Syntax Parser
55 | - Parsers fairly standardized syntax from [Lucene](https://lucene.apache.org/core/2_9_4/queryparsersyntax.html) and [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html).
56 | - Visitors for extensibility
57 | - Field Aliases (static and dynamic)
58 | - Query Includes
59 | - Define stored queries that can be included inside other queries as macros that will be expanded
60 | - Validation
61 | - Validate query syntax
62 | - Restrict access to specific fields
63 | - Restrict the number of operations allowed
64 | - Restrict nesting depth
65 | - Elasticsearch
66 | - Elastic query string query replacement on steriods
67 | - Dynamic search and filter expressions
68 | - Dynamic aggregation expressions
69 | - Supported bucket aggregations: terms, geo grid, date histogram, numeric histogram
70 | - Bucket aggregations allow nesting other dynamic aggregations inside
71 | - Supported metric aggregations: min, max, avg, sum, stats, extended stats, cardinality, missing, percentiles
72 | - Dynamic sort expressions
73 | - Dynamic expressions can be exposed to end users to allow for custom searches, filters, sorting and aggregations
74 | - Enables allowing users to build custom views, charts and dashboards
75 | - Enables powerful APIs that allow users to do things you never thought of
76 | - Supports geo queries (proximity and radius)
77 | - mygeo:75044~75mi
78 | - Returns all documents that have a value in the mygeo field that is within a 75 mile radius of the 75044 zip code
79 | - Supports nested document mappings
80 | - Automatically resolves non-analyzed keyword sub-fields for sorting and aggregations
81 | - Aliases can be defined right on your NEST mappings
82 | - Supports both root and inner field name aliases
83 |
84 | ## Thanks to all the people who have contributed
85 |
86 | [](https://github.com/FoundatioFx/Foundatio.Parsers/graphs/contributors)
87 |
--------------------------------------------------------------------------------
/build/Foundatio.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoundatioFx/Foundatio.Parsers/d75eb61dffc1e5e3ed65bfa519fc4854102adeed/build/Foundatio.snk
--------------------------------------------------------------------------------
/build/common.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;net8.0
5 | Foundatio.Parsers
6 | A lucene style query parser that is extensible and allows additional syntax features.
7 | https://github.com/FoundatioFx/Foundatio.Parsers
8 | https://github.com/FoundatioFx/Foundatio.Parsers/releases
9 | true
10 | v
11 | true
12 |
13 | Copyright (c) 2025 Foundatio. All rights reserved.
14 | FoundatioFx
15 | $(NoWarn);CS1591
16 | true
17 | latest
18 | true
19 | $(SolutionDir)artifacts
20 | foundatio-icon.png
21 | README.md
22 | Apache-2.0
23 | $(PackageProjectUrl)
24 | true
25 | true
26 | embedded
27 |
28 |
29 |
30 | true
31 |
32 |
33 |
34 | true
35 | $(MSBuildThisFileDirectory)Foundatio.snk
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/build/foundatio-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoundatioFx/Foundatio.Parsers/d75eb61dffc1e5e3ed65bfa519fc4854102adeed/build/foundatio-icon.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | elasticsearch:
3 | image: docker.elastic.co/elasticsearch/elasticsearch:8.18.1
4 | environment:
5 | discovery.type: single-node
6 | xpack.security.enabled: 'false'
7 | ES_JAVA_OPTS: -Xms512m -Xmx512m
8 | ports:
9 | - 9200:9200
10 | - 9300:9300
11 | networks:
12 | - foundatio
13 | healthcheck:
14 | interval: 2s
15 | retries: 10
16 | test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"'
17 |
18 | kibana:
19 | depends_on:
20 | elasticsearch:
21 | condition: service_healthy
22 | image: docker.elastic.co/kibana/kibana:8.18.1
23 | ports:
24 | - 5601:5601
25 | networks:
26 | - foundatio
27 | healthcheck:
28 | interval: 2s
29 | retries: 20
30 | test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:5601/api/status
31 |
32 | sqlserver:
33 | image: mcr.microsoft.com/mssql/server:2022-latest
34 | ports:
35 | - "1433:1433" # login with sa:P@ssword1
36 | environment:
37 | - "ACCEPT_EULA=Y"
38 | - "MSSQL_SA_PASSWORD=P@ssword1"
39 | - "MSSQL_PID=Developer"
40 | user: root
41 | networks:
42 | - foundatio
43 | healthcheck:
44 | test:
45 | [
46 | "CMD",
47 | "/opt/mssql-tools/bin/sqlcmd",
48 | "-Usa",
49 | "-PP@ssword1",
50 | "-Q",
51 | "select 1",
52 | ]
53 | interval: 1s
54 | retries: 20
55 |
56 | ready:
57 | image: andrewlock/wait-for-dependencies
58 | command: elasticsearch:9200
59 | depends_on:
60 | - elasticsearch
61 | - sqlserver
62 | networks:
63 | - foundatio
64 |
65 | networks:
66 | foundatio:
67 | driver: bridge
68 | name: foundatio
69 |
--------------------------------------------------------------------------------
/docs/query.md:
--------------------------------------------------------------------------------
1 | # Query Syntax
2 |
3 | The query syntax is based on Lucene syntax.
4 |
5 | ## Basic
6 | - `field:value` exact match
7 | - `field:"Eric Smith"` exact match with quoted string value
8 | - `_exists_:field` matches if there is any value for the field
9 | - `_missing_:field` matches if there is not a value for the field
10 |
11 | ## Ranges
12 | Ranges can be specified for date or numeric fields. Inclusive ranges are specified with square brackets `[min TO max]` and exclusive ranges with curly brackets `{min TO max}`.
13 |
14 | - `datefield:[2012-01-01 TO 2012-12-31]` matches all days in 2012 for `datefield`
15 | - `numberfield:[1 TO 5]` matches any number between 1 and 5 on `numberfield`
16 | - `numberfield:1..5` shorthand for above query
17 | - `numberfield:1..5` shorthand for above query
18 | - `datefield:{* TO 2012-01-01}` matches dates before 2012
19 | - `numberfield:[10 TO *]` matches values 10 and above
20 | - `numberfield:[1 TO 5}` matches numbers from 1 up to but not including 5
21 |
22 | Ranges with one side unbounded can use the following syntax:
23 |
24 | - `age:>10` matches age greater than 10
25 | - `age:>=10` matches age greater or equal to 10
26 | - `age:<10` matches age less than 10
27 | - `age:<=10` matches age less than or equal to 10
28 |
29 | ## Boolean operators
30 |
31 | `AND` and `OR` are used to combine filter criteria. `NOT` can be used to negate filter criteria
32 |
33 | `((field:quick AND otherfield:fox) OR (field:brown AND otherfield:fox) OR otherfield:fox) AND NOT thirdfield:news`
34 |
35 | ## Grouping
36 |
37 | Multiple terms or clauses can be grouped together with parentheses, to form sub-queries:
38 |
39 | `(field:quick OR field:brown) AND otherfield:fox`
40 |
41 | ## Date Math
42 |
43 | Parameters which accept a formatted date value understand date math.
44 |
45 | The expression starts with an anchor date, which can either be `now`, or a date string ending with ||. This anchor date can optionally be followed by one or more maths expressions:
46 |
47 | `+1h` Add one hour
48 | `-1d` Subtract one day
49 |
50 | Supported units are:
51 |
52 | - `y` Years
53 | - `M` Months
54 | - `w` Weeks
55 | - `d` Days
56 | - `h` Hours
57 | - `H` Hours
58 | - `m` Minutes
59 | - `s` Seconds
60 |
61 | Assuming now is `2001-01-01 12:00:00`, some examples are:
62 |
63 | - `now+1h` now in milliseconds plus one hour. Resolves to: 2001-01-01 13:00:00
64 | - `now-1h` now in milliseconds minus one hour. Resolves to: 2001-01-01 11:00:00
65 |
66 | ## Geo Proximity Queries
67 |
68 | Geo proximity queries allow filtering data by documents that have a geo field value located within a given proximity of a geo coordinate. Locations can be resolved using a provided geo coding function that can translate something like "Dallas, TX" into a geo coordinate.
69 |
70 | Examples:
71 | - Within 75 miles of abc geohash: geofield:abc~75mi
72 | - Within 75 miles of 75044: geofield:75044~75mi
73 |
74 | ## Geo Range Queries
75 |
76 | Geo range queries allow filtering data by documents that have a geo field value located within a bounding box. It uses the same syntax as other Lucene range queries, but the range is the top left and bottom right geo coordinates.
77 |
78 | Examples:
79 | - Within coordinates rectangle: geofield:[geohash1..geohash2]
80 |
81 | ## Nested Document Queries
82 |
83 | Elasticsearch does not support querying nested documents using the query_string query, but when using this library queries on those fields should work automatically.
84 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.100",
4 | "rollForward": "latestMinor",
5 | "allowPrerelease": false
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.ElasticQueries.Visitors;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 | using Foundatio.Parsers.LuceneQueries.Visitors;
7 | using Nest;
8 |
9 | namespace Foundatio.Parsers.ElasticQueries.Extensions;
10 |
11 | public static class DefaultQueryNodeExtensions
12 | {
13 | public static async Task GetDefaultQueryAsync(this IQueryNode node, IQueryVisitorContext context)
14 | {
15 | if (node is TermNode termNode)
16 | return termNode.GetDefaultQuery(context);
17 |
18 | if (node is TermRangeNode termRangeNode)
19 | return await termRangeNode.GetDefaultQueryAsync(context);
20 |
21 | if (node is ExistsNode existsNode)
22 | return existsNode.GetDefaultQuery(context);
23 |
24 | if (node is MissingNode missingNode)
25 | return missingNode.GetDefaultQuery(context);
26 |
27 | return null;
28 | }
29 |
30 | public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext context)
31 | {
32 | if (context is not IElasticQueryVisitorContext elasticContext)
33 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
34 |
35 | QueryBase query;
36 | string field = node.UnescapedField;
37 | string[] defaultFields = node.GetDefaultFields(elasticContext.DefaultFields);
38 | if (field == null && defaultFields != null && defaultFields.Length == 1)
39 | field = defaultFields[0];
40 |
41 | if (elasticContext.MappingResolver.IsPropertyAnalyzed(field))
42 | {
43 | string[] fields = !String.IsNullOrEmpty(field) ? [field] : defaultFields;
44 |
45 | if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*"))
46 | {
47 | query = new QueryStringQuery
48 | {
49 | Fields = fields,
50 | AllowLeadingWildcard = false,
51 | AnalyzeWildcard = true,
52 | Query = node.UnescapedTerm
53 | };
54 | }
55 | else
56 | {
57 | if (fields != null && fields.Length == 1)
58 | {
59 | if (node.IsQuotedTerm)
60 | {
61 | query = new MatchPhraseQuery
62 | {
63 | Field = fields[0],
64 | Query = node.UnescapedTerm
65 | };
66 | }
67 | else
68 | {
69 | query = new MatchQuery
70 | {
71 | Field = fields[0],
72 | Query = node.UnescapedTerm
73 | };
74 | }
75 | }
76 | else
77 | {
78 | query = new MultiMatchQuery
79 | {
80 | Fields = fields,
81 | Query = node.UnescapedTerm
82 | };
83 | if (node.IsQuotedTerm)
84 | ((MultiMatchQuery)query).Type = TextQueryType.Phrase;
85 | }
86 | }
87 | }
88 | else
89 | {
90 | if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*"))
91 | {
92 | query = new PrefixQuery
93 | {
94 | Field = field,
95 | Value = node.UnescapedTerm.TrimEnd('*')
96 | };
97 | }
98 | else
99 | {
100 | query = new TermQuery
101 | {
102 | Field = field,
103 | Value = node.UnescapedTerm
104 | };
105 | }
106 | }
107 |
108 | return query;
109 | }
110 |
111 | public static async Task GetDefaultQueryAsync(this TermRangeNode node, IQueryVisitorContext context)
112 | {
113 | if (context is not IElasticQueryVisitorContext elasticContext)
114 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
115 |
116 | string field = node.UnescapedField;
117 | if (elasticContext.MappingResolver.IsDatePropertyType(field))
118 | {
119 | var range = new DateRangeQuery { Field = field, TimeZone = node.Boost ?? node.GetTimeZone(await elasticContext.GetTimeZoneAsync()) };
120 | if (!String.IsNullOrWhiteSpace(node.UnescapedMin) && node.UnescapedMin != "*")
121 | {
122 | if (node.MinInclusive.HasValue && !node.MinInclusive.Value)
123 | range.GreaterThan = node.UnescapedMin;
124 | else
125 | range.GreaterThanOrEqualTo = node.UnescapedMin;
126 | }
127 |
128 | if (!String.IsNullOrWhiteSpace(node.UnescapedMax) && node.UnescapedMax != "*")
129 | {
130 | if (node.MaxInclusive.HasValue && !node.MaxInclusive.Value)
131 | range.LessThan = node.UnescapedMax;
132 | else
133 | range.LessThanOrEqualTo = node.UnescapedMax;
134 | }
135 |
136 | return range;
137 | }
138 | else
139 | {
140 | var range = new TermRangeQuery { Field = field };
141 | if (!String.IsNullOrWhiteSpace(node.UnescapedMin) && node.UnescapedMin != "*")
142 | {
143 | if (node.MinInclusive.HasValue && !node.MinInclusive.Value)
144 | range.GreaterThan = node.UnescapedMin;
145 | else
146 | range.GreaterThanOrEqualTo = node.UnescapedMin;
147 | }
148 |
149 | if (!String.IsNullOrWhiteSpace(node.UnescapedMax) && node.UnescapedMax != "*")
150 | {
151 | if (node.MaxInclusive.HasValue && !node.MaxInclusive.Value)
152 | range.LessThan = node.UnescapedMax;
153 | else
154 | range.LessThanOrEqualTo = node.UnescapedMax;
155 | }
156 |
157 | return range;
158 | }
159 | }
160 |
161 | public static QueryBase GetDefaultQuery(this ExistsNode node, IQueryVisitorContext context)
162 | {
163 | return new ExistsQuery { Field = node.UnescapedField };
164 | }
165 |
166 | public static QueryBase GetDefaultQuery(this MissingNode node, IQueryVisitorContext context)
167 | {
168 | return new BoolQuery
169 | {
170 | MustNot =
171 | [
172 | new ExistsQuery
173 | {
174 | Field = node.UnescapedField
175 | }
176 | ]
177 | };
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultSortNodeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Foundatio.Parsers.ElasticQueries.Visitors;
3 | using Foundatio.Parsers.LuceneQueries.Extensions;
4 | using Foundatio.Parsers.LuceneQueries.Nodes;
5 | using Foundatio.Parsers.LuceneQueries.Visitors;
6 | using Nest;
7 |
8 | namespace Foundatio.Parsers.ElasticQueries.Extensions;
9 |
10 | public static class DefaultSortNodeExtensions
11 | {
12 | public static IFieldSort GetDefaultSort(this TermNode node, IQueryVisitorContext context)
13 | {
14 | if (context is not IElasticQueryVisitorContext elasticContext)
15 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
16 |
17 | string field = elasticContext.MappingResolver.GetSortFieldName(node.UnescapedField);
18 | var fieldType = elasticContext.MappingResolver.GetFieldType(field);
19 |
20 | var sort = new FieldSort
21 | {
22 | Field = field,
23 | UnmappedType = fieldType == FieldType.None ? FieldType.Keyword : fieldType,
24 | Order = node.IsNodeOrGroupNegated() ? SortOrder.Descending : SortOrder.Ascending
25 | };
26 |
27 | return sort;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Extensions/ElasticMappingExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Nest;
2 |
3 | public static class ElasticMapping
4 | {
5 | ///
6 | /// Not chainable with AddSortField. Use AddKeywordAndSortFields to add both.
7 | ///
8 | public static TextPropertyDescriptor AddKeywordField(this TextPropertyDescriptor descriptor) where T : class
9 | {
10 | return descriptor.AddKeywordField(null);
11 | }
12 |
13 | ///
14 | /// Not chainable with AddSortField. Use AddKeywordAndSortFields to add both.
15 | ///
16 | public static TextPropertyDescriptor AddKeywordField(this TextPropertyDescriptor descriptor, string normalizer) where T : class
17 | {
18 | return descriptor.Fields(f => f.Keyword(s => s.Name(KeywordFieldName).Normalizer(normalizer).IgnoreAbove(256)));
19 | }
20 |
21 | ///
22 | /// Not chainable with AddSortField. Use AddKeywordAndSortFields to add both.
23 | ///
24 | public static TextPropertyDescriptor AddKeywordField(this TextPropertyDescriptor descriptor, bool lowercase) where T : class
25 | {
26 | return descriptor.AddKeywordField(lowercase ? "lowercase" : null);
27 | }
28 |
29 | ///
30 | /// Not chainable with AddKeywordField. Use AddKeywordAndSortFields to add both.
31 | ///
32 | public static TextPropertyDescriptor AddSortField(this TextPropertyDescriptor descriptor, string normalizer = "sort") where T : class
33 | {
34 | return descriptor.Fields(f => f.Keyword(s => s.Name(SortFieldName).Normalizer(normalizer).IgnoreAbove(256)));
35 | }
36 |
37 | public static TextPropertyDescriptor AddKeywordAndSortFields(this TextPropertyDescriptor descriptor, string sortNormalizer = "sort", string keywordNormalizer = null) where T : class
38 | {
39 | return descriptor.Fields(f => f
40 | .Keyword(s => s.Name(KeywordFieldName).Normalizer(keywordNormalizer).IgnoreAbove(256))
41 | .Keyword(s => s.Name(SortFieldName).Normalizer(sortNormalizer).IgnoreAbove(256)));
42 | }
43 |
44 | public static TextPropertyDescriptor AddKeywordAndSortFields(this TextPropertyDescriptor descriptor, bool keywordLowercase) where T : class
45 | {
46 | return descriptor.AddKeywordAndSortFields(keywordNormalizer: keywordLowercase ? "lowercase" : null);
47 | }
48 |
49 | public static AnalysisDescriptor AddSortNormalizer(this AnalysisDescriptor descriptor)
50 | {
51 | return descriptor.Normalizers(d => d.Custom("sort", n => n.Filters("lowercase", "asciifolding")));
52 | }
53 |
54 | public static string KeywordFieldName = "keyword";
55 | public static string SortFieldName = "sort";
56 | }
57 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Extensions/QueryNodeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Nodes;
4 | using Nest;
5 |
6 | namespace Foundatio.Parsers.ElasticQueries.Extensions;
7 |
8 | public static class QueryNodeExtensions
9 | {
10 | private const string QueryKey = "@Query";
11 | public static Task GetQueryAsync(this IQueryNode node, Func> getDefaultValue = null)
12 | {
13 | if (!node.Data.TryGetValue(QueryKey, out object value))
14 | {
15 | if (getDefaultValue == null)
16 | return Task.FromResult(null);
17 |
18 | return getDefaultValue?.Invoke();
19 | }
20 |
21 | return Task.FromResult(value as QueryBase);
22 | }
23 |
24 | public static void SetQuery(this IQueryNode node, QueryBase container)
25 | {
26 | node.Data[QueryKey] = container;
27 | }
28 |
29 | public static void RemoveQuery(this IQueryNode node)
30 | {
31 | if (node.Data.ContainsKey(QueryKey))
32 | node.Data.Remove(QueryKey);
33 | }
34 |
35 | private const string SourceFilterKey = "@SourceFilter";
36 | public static SourceFilter GetSourceFilter(this IQueryNode node, Func getDefaultValue = null)
37 | {
38 | if (!node.Data.TryGetValue(SourceFilterKey, out object value))
39 | return getDefaultValue?.Invoke();
40 |
41 | return value as SourceFilter;
42 | }
43 |
44 | private const string AggregationKey = "@Aggregation";
45 | public static Task GetAggregationAsync(this IQueryNode node, Func> getDefaultValue = null)
46 | {
47 | if (!node.Data.TryGetValue(AggregationKey, out object value))
48 | {
49 | if (getDefaultValue == null)
50 | return Task.FromResult(null);
51 |
52 | return getDefaultValue?.Invoke();
53 | }
54 |
55 | return Task.FromResult(value as AggregationBase);
56 | }
57 |
58 | public static void SetAggregation(this IQueryNode node, AggregationBase aggregation)
59 | {
60 | node.Data[AggregationKey] = aggregation;
61 | }
62 |
63 | public static void RemoveAggregation(this IQueryNode node)
64 | {
65 | node.Data.Remove(AggregationKey);
66 | }
67 |
68 | private const string SortKey = "@Sort";
69 | public static IFieldSort GetSort(this IQueryNode node, Func getDefaultValue = null)
70 | {
71 | if (!node.Data.TryGetValue(SortKey, out object value))
72 | return getDefaultValue?.Invoke();
73 |
74 | return value as IFieldSort;
75 | }
76 |
77 | public static void SetSort(this IQueryNode node, IFieldSort sort)
78 | {
79 | node.Data[SortKey] = sort;
80 | }
81 |
82 | public static void RemoveSort(this IQueryNode node)
83 | {
84 | node.Data.Remove(SortKey);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Extensions/QueryVisitorContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.ElasticQueries.Visitors;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 | using Foundatio.Parsers.LuceneQueries.Visitors;
7 | using Nest;
8 |
9 | namespace Foundatio.Parsers.ElasticQueries.Extensions;
10 |
11 | public static class QueryVisitorContextExtensions
12 | {
13 | public static bool? IsRuntimeFieldResolverEnabled(this T context) where T : IQueryVisitorContext
14 | {
15 | if (context is not IElasticQueryVisitorContext elasticContext)
16 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
17 |
18 | return elasticContext.EnableRuntimeFieldResolver;
19 | }
20 |
21 | public static RuntimeFieldResolver GetRuntimeFieldResolver(this IQueryVisitorContext context)
22 | {
23 | if (context is not IElasticQueryVisitorContext elasticContext)
24 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
25 |
26 | return elasticContext.RuntimeFieldResolver;
27 | }
28 |
29 | public static T EnableRuntimeFieldResolver(this T context, bool enabled = true) where T : IQueryVisitorContext
30 | {
31 | if (context is not IElasticQueryVisitorContext elasticContext)
32 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
33 |
34 | elasticContext.EnableRuntimeFieldResolver = enabled;
35 |
36 | return context;
37 | }
38 |
39 | public static T SetRuntimeFieldResolver(this T context, RuntimeFieldResolver resolver) where T : IQueryVisitorContext
40 | {
41 | if (context is not IElasticQueryVisitorContext elasticContext)
42 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
43 |
44 | elasticContext.RuntimeFieldResolver = resolver;
45 |
46 | return context;
47 | }
48 |
49 | public static Task GetTimeZoneAsync(this IQueryVisitorContext context)
50 | {
51 | var elasticContext = context as IElasticQueryVisitorContext;
52 | if (elasticContext?.DefaultTimeZone != null)
53 | return elasticContext.DefaultTimeZone.Invoke();
54 |
55 | return Task.FromResult(null);
56 | }
57 |
58 | public static T SetMappingResolver(this T context, ElasticMappingResolver mappingResolver) where T : IQueryVisitorContext
59 | {
60 | if (context is not IElasticQueryVisitorContext elasticContext)
61 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
62 |
63 | elasticContext.MappingResolver = mappingResolver ?? ElasticMappingResolver.NullInstance;
64 |
65 | return context;
66 | }
67 |
68 | public static ElasticMappingResolver GetMappingResolver(this T context) where T : IQueryVisitorContext
69 | {
70 | if (context is not IElasticQueryVisitorContext elasticContext)
71 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
72 |
73 | return elasticContext.MappingResolver ?? ElasticMappingResolver.NullInstance;
74 | }
75 |
76 | public static T UseSearchMode(this T context) where T : IQueryVisitorContext
77 | {
78 | context.SetDefaultOperator(GroupOperator.Or);
79 | context.UseScoring();
80 |
81 | return context;
82 | }
83 |
84 | public static T SetDefaultOperator(this T context, Operator defaultOperator) where T : IQueryVisitorContext
85 | {
86 | if (defaultOperator == Operator.And)
87 | context.DefaultOperator = GroupOperator.And;
88 | else if (defaultOperator == Operator.Or)
89 | context.DefaultOperator = GroupOperator.Or;
90 |
91 | return context;
92 | }
93 |
94 | public static T UseScoring(this T context) where T : IQueryVisitorContext
95 | {
96 | if (context is not IElasticQueryVisitorContext elasticContext)
97 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
98 |
99 | elasticContext.UseScoring = true;
100 |
101 | return context;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Nest;
3 |
4 | namespace Foundatio.Parsers.ElasticQueries.Extensions;
5 |
6 | public static class SearchDescriptorExtensions
7 | {
8 | public static SearchDescriptor Aggregations(this SearchDescriptor descriptor, AggregationContainer aggregations) where T : class
9 | {
10 | descriptor.Aggregations(f =>
11 | {
12 | ((IAggregationContainer)f).Aggregations = aggregations.Aggregations;
13 | return f;
14 | });
15 |
16 | return descriptor;
17 | }
18 |
19 | public static SearchDescriptor Sort(this SearchDescriptor descriptor, IEnumerable sorts) where T : class
20 | {
21 | var searchRequest = descriptor as ISearchRequest;
22 |
23 | foreach (var sort in sorts)
24 | {
25 | if (searchRequest.Sort == null)
26 | searchRequest.Sort = new List();
27 |
28 | searchRequest.Sort.Add(sort);
29 | }
30 |
31 | return descriptor;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Foundatio.Parsers.ElasticQueries.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Foundatio.Parsers.ElasticQueries.Extensions;
6 | using Foundatio.Parsers.LuceneQueries.Extensions;
7 | using Foundatio.Parsers.LuceneQueries.Nodes;
8 | using Foundatio.Parsers.LuceneQueries.Visitors;
9 | using Nest;
10 |
11 | namespace Foundatio.Parsers.ElasticQueries.Visitors;
12 |
13 | public class CombineAggregationsVisitor : ChainableQueryVisitor
14 | {
15 | public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
16 | {
17 | await base.VisitAsync(node, context).ConfigureAwait(false);
18 |
19 | if (context is not IElasticQueryVisitorContext elasticContext)
20 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
21 |
22 | var container = await GetParentContainerAsync(node, context);
23 | var termsAggregation = container as ITermsAggregation;
24 |
25 | foreach (var child in node.Children.OfType())
26 | {
27 | var aggregation = await child.GetAggregationAsync(() => child.GetDefaultAggregationAsync(context));
28 | if (aggregation == null)
29 | {
30 | var termNode = child as TermNode;
31 | if (termNode != null && termsAggregation != null)
32 | {
33 | // TODO: Move these to the default aggs method using a visitor to walk down the tree to gather them but not going into any sub groups
34 | if (termNode.Field == "@exclude")
35 | {
36 | termsAggregation.Exclude = termsAggregation.Exclude.AddValue(termNode.UnescapedTerm);
37 | }
38 | else if (termNode.Field == "@include")
39 | {
40 | termsAggregation.Include = termsAggregation.Include.AddValue(termNode.UnescapedTerm);
41 | }
42 | else if (termNode.Field == "@missing")
43 | {
44 | termsAggregation.Missing = termNode.UnescapedTerm;
45 | }
46 | else if (termNode.Field == "@min")
47 | {
48 | int? minCount = null;
49 | if (!String.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount))
50 | minCount = parsedMinCount;
51 |
52 | termsAggregation.MinimumDocumentCount = minCount;
53 | }
54 | }
55 |
56 | if (termNode != null && container is ITopHitsAggregation topHitsAggregation)
57 | {
58 | var filter = node.GetSourceFilter(() => new SourceFilter());
59 | if (termNode.Field == "@exclude")
60 | {
61 | if (filter.Excludes == null)
62 | filter.Excludes = termNode.UnescapedTerm;
63 | else
64 | filter.Excludes.And(termNode.UnescapedTerm);
65 | }
66 | else if (termNode.Field == "@include")
67 | {
68 | if (filter.Includes == null)
69 | filter.Includes = termNode.UnescapedTerm;
70 | else
71 | filter.Includes.And(termNode.UnescapedTerm);
72 | }
73 | topHitsAggregation.Source = filter;
74 | }
75 |
76 | if (termNode != null && container is IDateHistogramAggregation dateHistogramAggregation)
77 | {
78 | if (termNode.Field == "@missing")
79 | {
80 | DateTime? missingValue = null;
81 | if (!String.IsNullOrEmpty(termNode.Term) && DateTime.TryParse(termNode.Term, out var parsedMissingDate))
82 | missingValue = parsedMissingDate;
83 |
84 | dateHistogramAggregation.Missing = missingValue;
85 | }
86 | else if (termNode.Field == "@offset")
87 | {
88 | dateHistogramAggregation.Offset = termNode.IsExcluded() ? "-" + termNode.Term : termNode.Term;
89 | }
90 | }
91 |
92 | continue;
93 | }
94 |
95 | if (container is BucketAggregationBase bucketContainer)
96 | {
97 | if (bucketContainer.Aggregations == null)
98 | bucketContainer.Aggregations = new AggregationDictionary();
99 |
100 | bucketContainer.Aggregations[((IAggregation)aggregation).Name] = (AggregationContainer)aggregation;
101 | }
102 |
103 | if (termsAggregation != null && (child.Prefix == "-" || child.Prefix == "+"))
104 | {
105 | if (termsAggregation.Order == null)
106 | termsAggregation.Order = new List();
107 |
108 | termsAggregation.Order.Add(new TermsOrder
109 | {
110 | Key = ((IAggregation)aggregation).Name,
111 | Order = child.Prefix == "-" ? SortOrder.Descending : SortOrder.Ascending
112 | });
113 | }
114 | }
115 |
116 | if (node.Parent == null)
117 | node.SetAggregation(container);
118 | }
119 |
120 | private async Task GetParentContainerAsync(IQueryNode node, IQueryVisitorContext context)
121 | {
122 | AggregationBase container = null;
123 | var currentNode = node;
124 | while (container == null && currentNode != null)
125 | {
126 | IQueryNode n = currentNode;
127 | container = await n.GetAggregationAsync(async () =>
128 | {
129 | var result = await n.GetDefaultAggregationAsync(context);
130 | if (result != null)
131 | n.SetAggregation(result);
132 |
133 | return result;
134 | });
135 |
136 | if (currentNode.Parent != null)
137 | currentNode = currentNode.Parent;
138 | else
139 | break;
140 | }
141 |
142 | if (container == null)
143 | {
144 | container = new ChildrenAggregation(null, null);
145 | currentNode.SetAggregation(container);
146 | }
147 |
148 | return container;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.ElasticQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Extensions;
6 | using Foundatio.Parsers.LuceneQueries.Nodes;
7 | using Foundatio.Parsers.LuceneQueries.Visitors;
8 | using Nest;
9 |
10 | namespace Foundatio.Parsers.ElasticQueries.Visitors;
11 |
12 | public class CombineQueriesVisitor : ChainableQueryVisitor
13 | {
14 |
15 | public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
16 | {
17 | await base.VisitAsync(node, context).ConfigureAwait(false);
18 |
19 | // Only stop on scoped group nodes (parens). Gather all child queries (including scoped groups) and then combine them.
20 | // Combining only happens at the scoped group level though.
21 | // Merge all non-field terms together into a single match or multi-match query
22 | // Merge all nested queries for the same nested field together
23 |
24 | if (context is not IElasticQueryVisitorContext elasticContext)
25 | throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context));
26 |
27 | QueryBase query = await node.GetQueryAsync(() => node.GetDefaultQueryAsync(context)).ConfigureAwait(false);
28 | QueryBase container = query;
29 | var nested = query as NestedQuery;
30 | if (nested != null && node.Parent != null)
31 | container = null;
32 |
33 | foreach (var child in node.Children.OfType())
34 | {
35 | var childQuery = await child.GetQueryAsync(() => child.GetDefaultQueryAsync(context)).ConfigureAwait(false);
36 | if (childQuery == null) continue;
37 |
38 | var op = node.GetOperator(elasticContext);
39 | if (child.IsExcluded())
40 | childQuery = !childQuery;
41 |
42 | if (op == GroupOperator.Or && node.IsRequired())
43 | op = GroupOperator.And;
44 |
45 | if (op == GroupOperator.And)
46 | {
47 | container &= childQuery;
48 | }
49 | else if (op == GroupOperator.Or)
50 | {
51 | container |= childQuery;
52 | }
53 | }
54 |
55 | if (nested != null)
56 | {
57 | nested.Query = container;
58 | node.SetQuery(nested);
59 | }
60 | else
61 | {
62 | node.SetQuery(container);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/ElasticQueryVisitorContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | namespace Foundatio.Parsers.ElasticQueries.Visitors;
7 |
8 | public class ElasticQueryVisitorContext : QueryVisitorContext, IElasticQueryVisitorContext
9 | {
10 | public Func> DefaultTimeZone { get; set; }
11 | public bool UseScoring { get; set; }
12 | public ElasticMappingResolver MappingResolver { get; set; }
13 | public ICollection RuntimeFields { get; } = new List();
14 | public bool? EnableRuntimeFieldResolver { get; set; }
15 | public RuntimeFieldResolver RuntimeFieldResolver { get; set; }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/GeoVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.ElasticQueries.Extensions;
4 | using Foundatio.Parsers.LuceneQueries;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 | using Foundatio.Parsers.LuceneQueries.Visitors;
7 | using Nest;
8 |
9 | namespace Foundatio.Parsers.ElasticQueries.Visitors;
10 |
11 | public class GeoVisitor : ChainableQueryVisitor
12 | {
13 | private readonly Func> _resolveGeoLocation;
14 |
15 | public GeoVisitor(Func> resolveGeoLocation = null)
16 | {
17 | _resolveGeoLocation = resolveGeoLocation;
18 | }
19 |
20 | public override async Task VisitAsync(TermNode node, IQueryVisitorContext context)
21 | {
22 | if (context.QueryType != QueryTypes.Query || context is not IElasticQueryVisitorContext elasticContext || !elasticContext.MappingResolver.IsGeoPropertyType(node.Field))
23 | return;
24 |
25 | string location = _resolveGeoLocation != null ? await _resolveGeoLocation(node.Term).ConfigureAwait(false) ?? node.Term : node.Term;
26 | var query = new GeoDistanceQuery { Field = node.Field, Location = location, Distance = node.Proximity ?? Distance.Miles(10) };
27 | node.SetQuery(query);
28 | }
29 |
30 | public override void Visit(TermRangeNode node, IQueryVisitorContext context)
31 | {
32 | if (context is not IElasticQueryVisitorContext elasticContext || !elasticContext.MappingResolver.IsGeoPropertyType(node.Field))
33 | return;
34 |
35 | var box = new BoundingBox { TopLeft = node.Min, BottomRight = node.Max };
36 | var query = new GeoBoundingBoxQuery { BoundingBox = box, Field = node.Field };
37 | node.SetQuery(query);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/GetSortFieldsVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.ElasticQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 | using Foundatio.Parsers.LuceneQueries.Visitors;
7 | using Nest;
8 |
9 | namespace Foundatio.Parsers.ElasticQueries.Visitors;
10 |
11 | public class GetSortFieldsVisitor : QueryNodeVisitorWithResultBase>
12 | {
13 | private readonly List _fields = new();
14 |
15 | public override void Visit(TermNode node, IQueryVisitorContext context)
16 | {
17 | if (String.IsNullOrEmpty(node.Field))
18 | return;
19 |
20 | var sort = node.GetSort(() => node.GetDefaultSort(context));
21 | if (sort.SortKey == null)
22 | return;
23 |
24 | _fields.Add(sort);
25 | }
26 |
27 | public override async Task> AcceptAsync(IQueryNode node, IQueryVisitorContext context)
28 | {
29 | await node.AcceptAsync(this, context).ConfigureAwait(false);
30 | return _fields;
31 | }
32 |
33 | public static Task> RunAsync(IQueryNode node, IQueryVisitorContext context = null)
34 | {
35 | return new GetSortFieldsVisitor().AcceptAsync(node, context);
36 | }
37 |
38 | public static IEnumerable Run(IQueryNode node, IQueryVisitorContext context = null)
39 | {
40 | return RunAsync(node, context).GetAwaiter().GetResult();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/IElasticQueryVisitorContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | namespace Foundatio.Parsers.ElasticQueries.Visitors
7 | {
8 | public interface IElasticQueryVisitorContext : IQueryVisitorContext
9 | {
10 | Func> DefaultTimeZone { get; set; }
11 | bool UseScoring { get; set; }
12 | ElasticMappingResolver MappingResolver { get; set; }
13 | ICollection RuntimeFields { get; }
14 | bool? EnableRuntimeFieldResolver { get; set; }
15 | RuntimeFieldResolver RuntimeFieldResolver { get; set; }
16 | }
17 | }
18 |
19 | namespace Foundatio.Parsers
20 | {
21 | public delegate Task RuntimeFieldResolver(string field);
22 |
23 | public class ElasticRuntimeField
24 | {
25 | public string Name { get; set; }
26 | public ElasticRuntimeFieldType FieldType { get; set; } = ElasticRuntimeFieldType.Keyword;
27 | public string Script { get; set; }
28 | }
29 |
30 | // This is the list of supported field types for runtime fields:
31 | // https://www.elastic.co/guide/en/elasticsearch/reference/master/runtime-mapping-fields.html
32 | public enum ElasticRuntimeFieldType
33 | {
34 | Boolean,
35 | Date,
36 | Double,
37 | GeoPoint,
38 | Ip,
39 | Keyword,
40 | Long
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.ElasticQueries/Visitors/NestedVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.ElasticQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 | using Foundatio.Parsers.LuceneQueries.Visitors;
7 | using Nest;
8 |
9 | namespace Foundatio.Parsers.ElasticQueries.Visitors;
10 |
11 | public class NestedVisitor : ChainableQueryVisitor
12 | {
13 | public override Task VisitAsync(GroupNode node, IQueryVisitorContext context)
14 | {
15 | if (String.IsNullOrEmpty(node.Field))
16 | return base.VisitAsync(node, context);
17 |
18 | string nestedProperty = GetNestedProperty(node.Field, context);
19 | if (nestedProperty == null)
20 | return base.VisitAsync(node, context);
21 |
22 | node.SetQuery(new NestedQuery { Path = nestedProperty });
23 |
24 | return base.VisitAsync(node, context);
25 | }
26 |
27 | private string GetNestedProperty(string fullName, IQueryVisitorContext context)
28 | {
29 | string[] nameParts = fullName?.Split('.').ToArray();
30 |
31 | if (nameParts == null || context is not IElasticQueryVisitorContext elasticContext || nameParts.Length == 0)
32 | return null;
33 |
34 | string fieldName = String.Empty;
35 | for (int i = 0; i < nameParts.Length; i++)
36 | {
37 | if (i > 0)
38 | fieldName += ".";
39 |
40 | fieldName += nameParts[i];
41 |
42 | if (elasticContext.MappingResolver.IsNestedPropertyType(fieldName))
43 | return fieldName;
44 | }
45 |
46 | return null;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace Foundatio.Parsers.LuceneQueries.Extensions;
4 |
5 | public static class StringExtensions
6 | {
7 | public static string Unescape(this string input)
8 | {
9 | if (input == null)
10 | return null;
11 |
12 | var sb = new StringBuilder();
13 | bool escaped = false;
14 | foreach (char ch in input)
15 | {
16 | if (escaped)
17 | {
18 | sb.Append(ch);
19 | escaped = false;
20 | }
21 | else if (ch == '\\')
22 | {
23 | escaped = true;
24 | }
25 | else
26 | {
27 | sb.Append(ch);
28 | }
29 | }
30 |
31 | if (escaped)
32 | sb.Append('\\');
33 |
34 | return sb.ToString();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Extensions/TextWriterExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace Foundatio.Parsers.LuceneQueries.Extensions;
4 |
5 | public static class TextWriterExtensions
6 | {
7 | public static void WriteIf(this TextWriter writer, bool condition, string content, params object[] arguments)
8 | {
9 | if (!condition)
10 | return;
11 |
12 | if (arguments != null && arguments.Length > 0)
13 | writer.Write(content, arguments);
14 | else
15 | writer.Write(content);
16 | }
17 |
18 | public static void WriteLineIf(this TextWriter writer, bool condition, string content, params object[] arguments)
19 | {
20 | if (!condition)
21 | return;
22 |
23 | if (arguments != null && arguments.Length > 0)
24 | writer.WriteLine(content, arguments);
25 | else
26 | writer.WriteLine(content);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Foundatio.Parsers.LuceneQueries.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/LuceneQueryParser.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 | using Foundatio.Parsers.LuceneQueries.Visitors;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries;
6 |
7 | public partial class LuceneQueryParser : IQueryParser
8 | {
9 | public virtual Task ParseAsync(string query, IQueryVisitorContext context = null)
10 | {
11 | var result = Parse(query);
12 | return Task.FromResult(result);
13 | }
14 |
15 | public IQueryNode Parse(string query, IQueryVisitorContext context)
16 | {
17 | return ParseAsync(query, context).GetAwaiter().GetResult();
18 | }
19 | }
20 |
21 | public interface IQueryParser
22 | {
23 | Task ParseAsync(string query, IQueryVisitorContext context = null);
24 | }
25 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/ExistsNode.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text;
3 | using Foundatio.Parsers.LuceneQueries.Extensions;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | public class ExistsNode : QueryNodeBase, IFieldQueryNode
8 | {
9 | public bool? IsNegated { get; set; }
10 | public string Prefix { get; set; }
11 | public string Field { get; set; }
12 | public string UnescapedField => Field?.Unescape();
13 |
14 | public ExistsNode CopyTo(ExistsNode target)
15 | {
16 | if (IsNegated.HasValue)
17 | target.IsNegated = IsNegated;
18 |
19 | if (Prefix != null)
20 | target.Prefix = Prefix;
21 |
22 | if (Field != null)
23 | target.Field = Field;
24 |
25 | foreach (var kvp in Data)
26 | target.Data.Add(kvp.Key, kvp.Value);
27 |
28 | return target;
29 | }
30 |
31 | public override IQueryNode Clone()
32 | {
33 | var clone = new ExistsNode();
34 | CopyTo(clone);
35 | return clone;
36 | }
37 |
38 | public override string ToString()
39 | {
40 | var builder = new StringBuilder();
41 |
42 | if (IsNegated.HasValue && IsNegated.Value)
43 | builder.Append("NOT ");
44 |
45 | builder.Append(Prefix);
46 | builder.Append("_exists_");
47 | builder.Append(":");
48 | builder.Append(Field);
49 |
50 | return builder.ToString();
51 | }
52 |
53 | public override IEnumerable Children => EmptyNodeList;
54 | }
55 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/GroupNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
7 |
8 | public class GroupNode : QueryNodeBase, IFieldQueryWithProximityAndBoostNode
9 | {
10 | private IQueryNode _left;
11 | public IQueryNode Left
12 | {
13 | get => _left;
14 | set
15 | {
16 | _left = value;
17 | if (_left != null)
18 | _left.Parent = this;
19 | }
20 | }
21 |
22 | private IQueryNode _right;
23 | public IQueryNode Right
24 | {
25 | get => _right;
26 | set
27 | {
28 | _right = value;
29 | if (_right != null)
30 | _right.Parent = this;
31 | }
32 | }
33 |
34 | public GroupOperator Operator { get; set; } = GroupOperator.Default;
35 | public bool HasParens { get; set; }
36 | public string Field { get; set; }
37 | public string UnescapedField => Field?.Unescape();
38 | public bool? IsNegated { get; set; }
39 | public string Prefix { get; set; }
40 | public string Boost { get; set; }
41 | public string UnescapedBoost => Boost?.Unescape();
42 | public string Proximity { get; set; }
43 | public string UnescapedProximity => Proximity?.Unescape();
44 |
45 | public GroupNode CopyTo(GroupNode target)
46 | {
47 | if (Left != null)
48 | target.Left = Left.Clone();
49 |
50 | if (Right != null)
51 | target.Right = Right.Clone();
52 |
53 | target.Operator = Operator;
54 | target.HasParens = HasParens;
55 |
56 | if (Field != null)
57 | target.Field = Field;
58 |
59 | if (IsNegated.HasValue)
60 | target.IsNegated = IsNegated;
61 |
62 | if (Boost != null)
63 | target.Boost = Boost;
64 |
65 | if (Proximity != null)
66 | target.Proximity = Proximity;
67 |
68 | if (Prefix != null)
69 | target.Prefix = Prefix;
70 |
71 | foreach (var kvp in Data)
72 | target.Data.Add(kvp.Key, kvp.Value);
73 |
74 | return target;
75 | }
76 |
77 | public override string ToString()
78 | {
79 | return ToString(GroupOperator.Default);
80 | }
81 |
82 | public string ToString(GroupOperator defaultOperator)
83 | {
84 | if (Left == null && Right == null)
85 | return String.Empty;
86 |
87 | var builder = new StringBuilder();
88 | var op = Operator != GroupOperator.Default ? Operator : defaultOperator;
89 |
90 | if (IsNegated.HasValue && IsNegated.Value)
91 | builder.Append("NOT ");
92 |
93 | builder.Append(Prefix);
94 |
95 | if (!String.IsNullOrEmpty(Field))
96 | builder.Append(Field).Append(':');
97 |
98 | if (HasParens)
99 | builder.Append("(");
100 |
101 | if (Left != null)
102 | builder.Append(Left is GroupNode ? ((GroupNode)Left).ToString(defaultOperator) : Left.ToString());
103 |
104 | if (Left != null && Right != null)
105 | {
106 | if (op == GroupOperator.And)
107 | builder.Append(" AND ");
108 | else if (op == GroupOperator.Or)
109 | builder.Append(" OR ");
110 | else if (Right != null)
111 | builder.Append(" ");
112 | }
113 |
114 | if (Right != null)
115 | builder.Append(Right is GroupNode ? ((GroupNode)Right).ToString(defaultOperator) : Right.ToString());
116 |
117 | if (HasParens)
118 | builder.Append(")");
119 |
120 | if (Proximity != null)
121 | builder.Append("~" + Proximity);
122 |
123 | if (Boost != null)
124 | builder.Append("^" + Boost);
125 |
126 | return builder.ToString();
127 | }
128 |
129 | public override IQueryNode Clone()
130 | {
131 | var clone = new GroupNode();
132 | CopyTo(clone);
133 |
134 | return clone;
135 | }
136 |
137 | public override IEnumerable Children
138 | {
139 | get
140 | {
141 | var children = new List();
142 | if (Left != null)
143 | children.Add(Left);
144 | if (Right != null)
145 | children.Add(Right);
146 |
147 | return children;
148 | }
149 | }
150 | }
151 |
152 | public enum GroupOperator
153 | {
154 | Default,
155 | And,
156 | Or
157 | }
158 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/IQueryNode.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Visitors;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | public interface IQueryNode
8 | {
9 | IQueryNode Parent { get; set; }
10 | IEnumerable Children { get; }
11 | IDictionary Data { get; }
12 | Task AcceptAsync(IQueryNodeVisitor visitor, IQueryVisitorContext context);
13 | string ToString();
14 | IQueryNode Clone();
15 | }
16 |
17 | public interface IFieldQueryNode : IQueryNode
18 | {
19 | bool? IsNegated { get; set; }
20 | string Prefix { get; set; }
21 | string Field { get; set; }
22 | string UnescapedField { get; }
23 | }
24 |
25 | public interface IFieldQueryWithProximityAndBoostNode : IFieldQueryNode
26 | {
27 | string Boost { get; set; }
28 | string UnescapedBoost { get; }
29 | string Proximity { get; set; }
30 | string UnescapedProximity { get; }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/MissingNode.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text;
3 | using Foundatio.Parsers.LuceneQueries.Extensions;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | public class MissingNode : QueryNodeBase, IFieldQueryNode
8 | {
9 | public bool? IsNegated { get; set; }
10 | public string Prefix { get; set; }
11 | public string Field { get; set; }
12 | public string UnescapedField => Field?.Unescape();
13 |
14 | public MissingNode CopyTo(MissingNode target)
15 | {
16 | if (IsNegated.HasValue)
17 | target.IsNegated = IsNegated;
18 |
19 | if (Prefix != null)
20 | target.Prefix = Prefix;
21 |
22 | if (Field != null)
23 | target.Field = Field;
24 |
25 | foreach (var kvp in Data)
26 | target.Data.Add(kvp.Key, kvp.Value);
27 |
28 | return target;
29 | }
30 |
31 | public override IQueryNode Clone()
32 | {
33 | var clone = new MissingNode();
34 | CopyTo(clone);
35 | return clone;
36 | }
37 |
38 | public override string ToString()
39 | {
40 | var builder = new StringBuilder();
41 |
42 | if (IsNegated.HasValue && IsNegated.Value)
43 | builder.Append("NOT ");
44 |
45 | builder.Append(Prefix);
46 | builder.Append("_missing_");
47 | builder.Append(":");
48 | builder.Append(Field);
49 |
50 | return builder.ToString();
51 | }
52 |
53 | public override IEnumerable Children => EmptyNodeList;
54 | }
55 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/QueryNodeBase.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Visitors;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | public abstract class QueryNodeBase : IQueryNode
8 | {
9 | public virtual Task AcceptAsync(IQueryNodeVisitor visitor, IQueryVisitorContext context)
10 | {
11 | if (this is GroupNode groupNode)
12 | return visitor.VisitAsync(groupNode, context);
13 |
14 | if (this is TermNode termNode)
15 | return visitor.VisitAsync(termNode, context);
16 |
17 | if (this is TermRangeNode rangeNode)
18 | return visitor.VisitAsync(rangeNode, context);
19 |
20 | if (this is MissingNode missingNode)
21 | return visitor.VisitAsync(missingNode, context);
22 |
23 | if (this is ExistsNode existsNode)
24 | return visitor.VisitAsync(existsNode, context);
25 |
26 | return Task.FromResult(this);
27 | }
28 |
29 | public IDictionary Data { get; } = new Dictionary();
30 | public abstract IEnumerable Children { get; }
31 | public IQueryNode Parent { get; set; }
32 | public static readonly IList EmptyNodeList = new List().AsReadOnly();
33 |
34 | public abstract IQueryNode Clone();
35 | public abstract override string ToString();
36 | }
37 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/TermNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
7 |
8 | public class TermNode : QueryNodeBase, IFieldQueryWithProximityAndBoostNode
9 | {
10 | public bool? IsNegated { get; set; }
11 | public string Prefix { get; set; }
12 | public string Field { get; set; }
13 | public string UnescapedField => Field?.Unescape();
14 | public string Term { get; set; }
15 | public string UnescapedTerm => Term?.Unescape();
16 | public bool IsQuotedTerm { get; set; }
17 | public bool IsRegexTerm { get; set; }
18 | public string Boost { get; set; }
19 | public string UnescapedBoost => Boost?.Unescape();
20 | public string Proximity { get; set; }
21 | public string UnescapedProximity => Proximity?.Unescape();
22 |
23 | public TermNode CopyTo(TermNode target)
24 | {
25 | if (IsNegated.HasValue)
26 | target.IsNegated = IsNegated;
27 |
28 | if (Prefix != null)
29 | target.Prefix = Prefix;
30 |
31 | if (Field != null)
32 | target.Field = Field;
33 |
34 | if (Term != null)
35 | target.Term = Term;
36 |
37 | target.IsQuotedTerm = IsQuotedTerm;
38 | target.IsRegexTerm = IsRegexTerm;
39 |
40 | if (Boost != null)
41 | target.Boost = Boost;
42 |
43 | if (Proximity != null)
44 | target.Proximity = Proximity;
45 |
46 | foreach (var kvp in Data)
47 | target.Data.Add(kvp.Key, kvp.Value);
48 |
49 | return target;
50 | }
51 |
52 | public override string ToString()
53 | {
54 | var builder = new StringBuilder();
55 |
56 | if (IsNegated.HasValue && IsNegated.Value)
57 | builder.Append("NOT ");
58 |
59 | builder.Append(Prefix);
60 |
61 | if (!String.IsNullOrEmpty(Field))
62 | {
63 | builder.Append(Field);
64 | builder.Append(":");
65 | }
66 |
67 | if (IsQuotedTerm)
68 | builder.Append("\"" + Term + "\"");
69 | else if (IsRegexTerm)
70 | builder.Append("/" + Term + "/");
71 | else
72 | builder.Append(Term);
73 |
74 | if (Proximity != null)
75 | builder.Append("~" + Proximity);
76 |
77 | if (Boost != null)
78 | builder.Append("^" + Boost);
79 |
80 | return builder.ToString();
81 | }
82 |
83 | public override IQueryNode Clone()
84 | {
85 | var clone = new TermNode();
86 | CopyTo(clone);
87 | return clone;
88 | }
89 |
90 | public override IEnumerable Children => EmptyNodeList;
91 | }
92 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Nodes/TermRangeNode.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Nodes;
7 |
8 | public class TermRangeNode : QueryNodeBase, IFieldQueryNode
9 | {
10 | public bool? IsNegated { get; set; }
11 | public string Field { get; set; }
12 | public string UnescapedField => Field?.Unescape();
13 | public string Prefix { get; set; }
14 | public string Min { get; set; }
15 | public string UnescapedMin => Min?.Unescape();
16 | public bool IsMinQuotedTerm { get; set; }
17 | public string Max { get; set; }
18 | public string UnescapedMax => Max?.Unescape();
19 | public bool IsMaxQuotedTerm { get; set; }
20 | public string Operator { get; set; }
21 | public string Delimiter { get; set; }
22 | public bool? MinInclusive { get; set; }
23 | public bool? MaxInclusive { get; set; }
24 | public string Boost { get; set; }
25 | public string UnescapedBoost => Boost?.Unescape();
26 | public string Proximity { get; set; }
27 |
28 | public TermRangeNode CopyTo(TermRangeNode target)
29 | {
30 | if (Field != null)
31 | target.Field = Field;
32 |
33 | if (IsNegated.HasValue)
34 | target.IsNegated = IsNegated;
35 |
36 | if (Prefix != null)
37 | target.Prefix = Prefix;
38 |
39 | if (Min != null)
40 | target.Min = Min;
41 |
42 | target.IsMinQuotedTerm = IsMinQuotedTerm;
43 |
44 | if (Max != null)
45 | target.Max = Max;
46 |
47 | target.IsMaxQuotedTerm = IsMaxQuotedTerm;
48 |
49 | if (Operator != null)
50 | target.Operator = Operator;
51 |
52 | if (Delimiter != null)
53 | target.Delimiter = Delimiter;
54 |
55 | if (MinInclusive.HasValue)
56 | target.MinInclusive = MinInclusive;
57 |
58 | if (MaxInclusive.HasValue)
59 | target.MaxInclusive = MaxInclusive;
60 |
61 | if (Boost != null)
62 | target.Boost = Boost;
63 |
64 | if (Proximity != null)
65 | target.Proximity = Proximity;
66 |
67 | foreach (var kvp in Data)
68 | target.Data.Add(kvp.Key, kvp.Value);
69 |
70 | return target;
71 | }
72 |
73 | public override IQueryNode Clone()
74 | {
75 | var clone = new TermRangeNode();
76 | CopyTo(clone);
77 | return clone;
78 | }
79 |
80 | public override string ToString()
81 | {
82 | var builder = new StringBuilder();
83 |
84 | if (IsNegated.HasValue && IsNegated.Value)
85 | builder.Append("NOT ");
86 |
87 | builder.Append(Prefix);
88 |
89 | if (!String.IsNullOrEmpty(Field))
90 | {
91 | builder.Append(Field);
92 | builder.Append(":");
93 | }
94 |
95 | if (!String.IsNullOrEmpty(Operator))
96 | builder.Append(Operator);
97 |
98 | if (MinInclusive.HasValue && String.IsNullOrEmpty(Operator))
99 | builder.Append(MinInclusive.Value ? "[" : "{");
100 |
101 | if (IsMinQuotedTerm)
102 | builder.Append("\"" + Min + "\"");
103 | else
104 | builder.Append(Min);
105 |
106 | if (!String.IsNullOrEmpty(Min) && !String.IsNullOrEmpty(Max) && String.IsNullOrEmpty(Operator))
107 | builder.Append(Delimiter ?? " TO ");
108 |
109 | if (IsMaxQuotedTerm)
110 | builder.Append("\"" + Max + "\"");
111 | else
112 | builder.Append(Max);
113 |
114 | if (MaxInclusive.HasValue && String.IsNullOrEmpty(Operator))
115 | builder.Append(MaxInclusive.Value ? "]" : "}");
116 |
117 | if (Proximity != null)
118 | builder.Append("~" + Proximity);
119 |
120 | if (Boost != null)
121 | builder.Append("^" + Boost);
122 |
123 | return builder.ToString();
124 | }
125 |
126 | public override IEnumerable Children => EmptyNodeList;
127 | }
128 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/QueryTypes.cs:
--------------------------------------------------------------------------------
1 | namespace Foundatio.Parsers.LuceneQueries;
2 |
3 | public static class QueryTypes
4 | {
5 | public const string Query = "query";
6 | public const string Aggregation = "aggregation";
7 | public const string Sort = "sort";
8 | public const string Unknown = "unknown";
9 | }
10 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/QueryValidation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 |
7 | namespace Foundatio.Parsers.LuceneQueries;
8 |
9 | public class QueryValidationOptions
10 | {
11 | public bool ShouldThrow { get; set; }
12 | public ICollection AllowedFields { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
13 | public ICollection RestrictedFields { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
14 | public bool AllowLeadingWildcards { get; set; } = true;
15 | public bool AllowUnresolvedFields { get; set; } = true;
16 | public bool AllowUnresolvedIncludes { get; set; } = false;
17 | public ICollection AllowedOperations { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
18 | public ICollection RestrictedOperations { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
19 | public int AllowedMaxNodeDepth { get; set; }
20 | }
21 |
22 | [DebuggerDisplay("IsValid: {IsValid} Message: {Message} Type: {QueryType}")]
23 | public class QueryValidationResult
24 | {
25 | private ConcurrentDictionary> _operations = new(StringComparer.OrdinalIgnoreCase);
26 |
27 | public string QueryType { get; set; }
28 | public bool IsValid => ValidationErrors.Count == 0;
29 | public ICollection ValidationErrors { get; } = new List();
30 | public string Message
31 | {
32 | get
33 | {
34 | if (ValidationErrors.Count == 0)
35 | return String.Empty;
36 |
37 | if (ValidationErrors.Count > 1)
38 | return String.Join("\r\n", ValidationErrors.Select(e => e.ToString()));
39 |
40 | return ValidationErrors.Single().Message;
41 | }
42 | }
43 | public ICollection ReferencedFields { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
44 | public ICollection ReferencedIncludes { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
45 | public ICollection UnresolvedFields { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
46 | public ICollection UnresolvedIncludes { get; } = new HashSet(StringComparer.OrdinalIgnoreCase);
47 | public int MaxNodeDepth { get; set; } = 1;
48 | public IDictionary> Operations => _operations;
49 |
50 | public static implicit operator bool(QueryValidationResult info) => info.IsValid;
51 |
52 | private int _currentNodeDepth = 1;
53 | internal int CurrentNodeDepth
54 | {
55 | get => _currentNodeDepth;
56 | set
57 | {
58 | _currentNodeDepth = value;
59 | if (_currentNodeDepth > MaxNodeDepth)
60 | MaxNodeDepth = _currentNodeDepth;
61 | }
62 | }
63 |
64 | internal void AddOperation(string operation, string field)
65 | {
66 | _operations.AddOrUpdate(operation,
67 | _ => new HashSet(StringComparer.OrdinalIgnoreCase) { field },
68 | (_, collection) =>
69 | {
70 | collection.Add(field);
71 | return collection;
72 | }
73 | );
74 | }
75 | }
76 |
77 | public class QueryValidationError
78 | {
79 | public QueryValidationError(string message, int index = -1)
80 | {
81 | Message = message;
82 | Index = index;
83 | }
84 |
85 | ///
86 | /// The validation error message.
87 | ///
88 | public string Message { get; }
89 |
90 | ///
91 | /// Index where the validation error occurs in the query string.
92 | ///
93 | public int Index { get; } = -1;
94 |
95 | public override string ToString()
96 | {
97 | if (Index > 0)
98 | return $"[{Index}] {Message}";
99 |
100 | return Message;
101 | }
102 | }
103 |
104 | public class QueryValidationException : Exception
105 | {
106 | public QueryValidationException(string message, QueryValidationResult result = null,
107 | Exception inner = null) : base(message, inner)
108 | {
109 | Result = result;
110 | }
111 |
112 | public QueryValidationResult Result { get; }
113 | public ICollection Errors => Result.ValidationErrors;
114 | }
115 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/QueryValidator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Extensions;
4 | using Foundatio.Parsers.LuceneQueries.Visitors;
5 | using Pegasus.Common;
6 |
7 | namespace Foundatio.Parsers.LuceneQueries;
8 |
9 | public class QueryValidator
10 | {
11 | public static Task ValidateQueryAsync(string query, QueryValidationOptions options = null, IQueryVisitorContextWithValidation context = null)
12 | {
13 | if (context == null)
14 | context = new QueryVisitorContext();
15 |
16 | context.QueryType = QueryTypes.Query;
17 |
18 | return InternalValidateAsync(query, context, options);
19 | }
20 |
21 | public static Task ValidateAggregationsAsync(string aggregations, QueryValidationOptions options = null, IQueryVisitorContextWithValidation context = null)
22 | {
23 | if (context == null)
24 | context = new QueryVisitorContext();
25 |
26 | context.QueryType = QueryTypes.Aggregation;
27 |
28 | return InternalValidateAsync(aggregations, context, options);
29 | }
30 |
31 | public static Task ValidateSortAsync(string sort, QueryValidationOptions options = null, IQueryVisitorContextWithValidation context = null)
32 | {
33 | if (context == null)
34 | context = new QueryVisitorContext();
35 |
36 | context.QueryType = QueryTypes.Sort;
37 |
38 | return InternalValidateAsync(sort, context, options);
39 | }
40 |
41 | private static async Task InternalValidateAsync(string query, IQueryVisitorContextWithValidation context, QueryValidationOptions options = null)
42 | {
43 | var parser = new LuceneQueryParser();
44 | try
45 | {
46 | var node = await parser.ParseAsync(query);
47 | if (context == null)
48 | context = new QueryVisitorContext();
49 |
50 | if (options != null)
51 | context.SetValidationOptions(options);
52 |
53 | var fieldResolver = context.GetFieldResolver();
54 | if (fieldResolver != null)
55 | node = await FieldResolverQueryVisitor.RunAsync(node, fieldResolver, context as IQueryVisitorContextWithFieldResolver);
56 |
57 | var includeResolver = context.GetIncludeResolver();
58 | if (includeResolver != null)
59 | node = await IncludeVisitor.RunAsync(node, includeResolver, context as IQueryVisitorContextWithIncludeResolver);
60 |
61 | return await ValidationVisitor.RunAsync(node, context);
62 | }
63 | catch (FormatException ex)
64 | {
65 | var cursor = ex.Data["cursor"] as Cursor;
66 | context.AddValidationError(ex.Message, cursor.Column);
67 |
68 | return context.GetValidationResult();
69 | }
70 | }
71 |
72 | public static Task ValidateQueryAndThrowAsync(string query, QueryValidationOptions options = null, IQueryVisitorContextWithValidation context = null)
73 | {
74 | if (context == null)
75 | context = new QueryVisitorContext();
76 |
77 | context.QueryType = QueryTypes.Query;
78 |
79 | return InternalValidateAndThrowAsync(query, context, options);
80 | }
81 |
82 | public static Task ValidateAggregationsAndThrowAsync(string aggregations, QueryValidationOptions options = null, IQueryVisitorContextWithValidation context = null)
83 | {
84 | if (context == null)
85 | context = new QueryVisitorContext();
86 |
87 | context.QueryType = QueryTypes.Aggregation;
88 |
89 | return InternalValidateAndThrowAsync(aggregations, context, options);
90 | }
91 |
92 | public static Task ValidateSortAndThrowAsync(string sort, QueryValidationOptions options = null, IQueryVisitorContextWithValidation context = null)
93 | {
94 | if (context == null)
95 | context = new QueryVisitorContext();
96 |
97 | context.QueryType = QueryTypes.Sort;
98 |
99 | return InternalValidateAndThrowAsync(sort, context, options);
100 | }
101 |
102 | private static async Task InternalValidateAndThrowAsync(string query, IQueryVisitorContextWithValidation context, QueryValidationOptions options = null)
103 | {
104 | var parser = new LuceneQueryParser();
105 | try
106 | {
107 | var node = await parser.ParseAsync(query);
108 | if (context == null)
109 | context = new QueryVisitorContext();
110 |
111 | options ??= new QueryValidationOptions();
112 | options.ShouldThrow = true;
113 | context.SetValidationOptions(options);
114 |
115 | var fieldResolver = context.GetFieldResolver();
116 | if (fieldResolver != null)
117 | node = await FieldResolverQueryVisitor.RunAsync(node, fieldResolver, context as IQueryVisitorContextWithFieldResolver);
118 |
119 | var includeResolver = context.GetIncludeResolver();
120 | if (includeResolver != null)
121 | node = await IncludeVisitor.RunAsync(node, includeResolver, context as IQueryVisitorContextWithIncludeResolver);
122 |
123 | return await ValidationVisitor.RunAsync(node, context);
124 | }
125 | catch (FormatException ex)
126 | {
127 | var cursor = ex.Data["cursor"] as Cursor;
128 | context.AddValidationError(ex.Message, cursor.Column);
129 |
130 | throw new QueryValidationException(ex.Message, context.GetValidationResult(), ex);
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/AssignOperationTypeVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Extensions;
4 | using Foundatio.Parsers.LuceneQueries.Nodes;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
7 |
8 | public class AssignOperationTypeVisitor : ChainableQueryVisitor
9 | {
10 | public override Task VisitAsync(GroupNode node, IQueryVisitorContext context)
11 | {
12 | if (node.HasOperationType())
13 | return Task.CompletedTask;
14 |
15 | if (String.IsNullOrEmpty(node.Field))
16 | return base.VisitAsync(node, context);
17 |
18 | if (node.Left is not TermNode leftTerm)
19 | {
20 | context.AddValidationError($"Aggregations ({node.Field}) must specify a field.");
21 | return Task.CompletedTask;
22 | }
23 |
24 | if (String.IsNullOrEmpty(leftTerm.Term))
25 | context.AddValidationError($"Aggregations ({node.Field}) must specify a field.");
26 |
27 | if (node.Field.StartsWith("@"))
28 | return base.VisitAsync(node, context);
29 |
30 | node.SetOperationType(node.Field);
31 | node.Field = leftTerm.Term;
32 | node.Boost = leftTerm.Boost;
33 | node.Proximity = leftTerm.Proximity;
34 | node.Left = null;
35 |
36 | return base.VisitAsync(node, context);
37 | }
38 |
39 | public override void Visit(TermNode node, IQueryVisitorContext context)
40 | {
41 | if (node.HasOperationType())
42 | return;
43 |
44 | if (String.IsNullOrEmpty(node.Field) && !String.IsNullOrEmpty(node.Term))
45 | {
46 | context.AddValidationError($"Aggregations ({node.Term}) must specify a field.");
47 | node.SetOperationType(node.Term);
48 | return;
49 | }
50 |
51 | if (String.IsNullOrEmpty(node.Term))
52 | context.AddValidationError($"Aggregations ({node.Field}) must specify a field.");
53 |
54 | if (node.Field != null && node.Field.StartsWith("@"))
55 | return;
56 |
57 | node.SetOperationType(node.Field);
58 | node.Field = node.Term;
59 | node.Term = null;
60 | }
61 | }
62 |
63 | public static class AggregationType
64 | {
65 | public const string Min = "min";
66 | public const string Max = "max";
67 | public const string Avg = "avg";
68 | public const string Sum = "sum";
69 | public const string Cardinality = "cardinality";
70 | public const string Missing = "missing";
71 | public const string DateHistogram = "date";
72 | public const string Histogram = "histogram";
73 | public const string Percentiles = "percentiles";
74 | public const string GeoHashGrid = "geogrid";
75 | public const string Terms = "terms";
76 | public const string Stats = "stats";
77 | public const string TopHits = "tophits";
78 | public const string ExtendedStats = "exstats";
79 | }
80 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/ChainedQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Nodes;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
7 |
8 | public class ChainedQueryVisitor : QueryNodeVisitorWithResultBase, IChainableQueryVisitor
9 | {
10 | private readonly List _visitors = new();
11 | private QueryVisitorWithPriority[] _frozenVisitors;
12 | private bool _isDirty = true;
13 |
14 | public void AddVisitor(IQueryNodeVisitorWithResult visitor, int priority = 0)
15 | {
16 | AddVisitor(new QueryVisitorWithPriority
17 | {
18 | Priority = priority,
19 | Visitor = visitor
20 | });
21 | }
22 |
23 | public void AddVisitor(QueryVisitorWithPriority visitor)
24 | {
25 | _visitors.Add(visitor);
26 | _isDirty = true;
27 | }
28 |
29 | public void RemoveVisitor() where T : IChainableQueryVisitor
30 | {
31 | var visitor = _visitors.FirstOrDefault(v => typeof(T) == v.Visitor.GetType());
32 | if (visitor == null)
33 | return;
34 |
35 | _visitors.Remove(visitor);
36 | _isDirty = true;
37 | }
38 |
39 | public void ReplaceVisitor(IChainableQueryVisitor visitor, int? newPriority = null) where T : IChainableQueryVisitor
40 | {
41 | int priority = newPriority.GetValueOrDefault(0);
42 |
43 | var referenceVisitor = _visitors.FirstOrDefault(v => typeof(T) == v.Visitor.GetType());
44 | if (referenceVisitor != null)
45 | {
46 | if (!newPriority.HasValue)
47 | priority = referenceVisitor.Priority - 1;
48 |
49 | _visitors.Remove(referenceVisitor);
50 | }
51 |
52 | _visitors.Add(new QueryVisitorWithPriority { Visitor = visitor, Priority = priority });
53 | _isDirty = true;
54 | }
55 |
56 | public void AddVisitorBefore(IChainableQueryVisitor visitor)
57 | {
58 | int priority = 0;
59 | var referenceVisitor = _visitors.FirstOrDefault(v => typeof(T) == v.Visitor.GetType());
60 | if (referenceVisitor != null)
61 | priority = referenceVisitor.Priority - 1;
62 |
63 | _visitors.Add(new QueryVisitorWithPriority { Visitor = visitor, Priority = priority });
64 | _isDirty = true;
65 | }
66 |
67 | public void AddVisitorAfter(IChainableQueryVisitor visitor)
68 | {
69 | int priority = 0;
70 | var referenceVisitor = _visitors.FirstOrDefault(v => typeof(T) == v.Visitor.GetType());
71 | if (referenceVisitor != null)
72 | priority = referenceVisitor.Priority + 1;
73 |
74 | _visitors.Add(new QueryVisitorWithPriority { Visitor = visitor, Priority = priority });
75 | _isDirty = true;
76 | }
77 |
78 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
79 | {
80 | if (_isDirty)
81 | _frozenVisitors = _visitors.OrderBy(v => v.Priority).ToArray();
82 |
83 | foreach (var visitor in _frozenVisitors)
84 | node = await visitor.AcceptAsync(node, context).ConfigureAwait(false);
85 |
86 | return node;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/CleanupQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Extensions;
4 | using Foundatio.Parsers.LuceneQueries.Nodes;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
7 |
8 | public class CleanupQueryVisitor : ChainableQueryVisitor
9 | {
10 | public override async Task VisitAsync(IQueryNode node, IQueryVisitorContext context)
11 | {
12 | var result = CleanNode(node);
13 | if (result == null)
14 | return null;
15 |
16 | result = await base.VisitAsync(result, context);
17 |
18 | return CleanNode(result);
19 | }
20 |
21 | private IQueryNode CleanNode(IQueryNode node)
22 | {
23 | if (node is GroupNode groupNode)
24 | {
25 | if (groupNode.Left != null && groupNode.Right != null)
26 | return groupNode;
27 |
28 | // remove non-root empty groups
29 | if (groupNode.Left == null && groupNode.Right == null && groupNode.Parent != null)
30 | {
31 | groupNode.RemoveSelf();
32 | return groupNode.Parent;
33 | }
34 |
35 | // don't alter groups with fields
36 | if (!String.IsNullOrEmpty(groupNode.Field))
37 | {
38 | return groupNode;
39 | }
40 |
41 | if (groupNode.Left is GroupNode leftGroupNode && groupNode.Right == null && leftGroupNode.Field == null)
42 | {
43 | leftGroupNode.Field = groupNode.Field;
44 | groupNode.ReplaceSelf(leftGroupNode);
45 |
46 | // no value is set, use parent value
47 | if (!leftGroupNode.IsNegated.HasValue)
48 | {
49 | leftGroupNode.IsNegated = groupNode.IsNegated;
50 | }
51 | else if (groupNode.IsNegated.HasValue)
52 | {
53 | // both values set
54 |
55 | // double negative
56 | if (groupNode.IsNegated.Value && leftGroupNode.IsNegated.Value)
57 | leftGroupNode.IsNegated = false;
58 | else if (groupNode.IsNegated.Value || leftGroupNode.IsNegated.Value)
59 | leftGroupNode.IsNegated = true;
60 | }
61 |
62 | groupNode = leftGroupNode;
63 | node = groupNode;
64 | }
65 |
66 | if (groupNode.Right is GroupNode rightGroupNode && groupNode.Left == null && rightGroupNode.Field == null)
67 | {
68 | rightGroupNode.Field = groupNode.Field;
69 | groupNode.ReplaceSelf(rightGroupNode);
70 |
71 | // no value is set, use parent value
72 | if (!rightGroupNode.IsNegated.HasValue)
73 | {
74 | rightGroupNode.IsNegated = groupNode.IsNegated;
75 | }
76 | else if (groupNode.IsNegated.HasValue)
77 | {
78 | // both values set
79 |
80 | // double negative
81 | if (groupNode.IsNegated.Value && rightGroupNode.IsNegated.Value)
82 | rightGroupNode.IsNegated = false;
83 | else if (groupNode.IsNegated.Value || rightGroupNode.IsNegated.Value)
84 | rightGroupNode.IsNegated = true;
85 | }
86 |
87 | groupNode = rightGroupNode;
88 | node = groupNode;
89 | }
90 |
91 | // don't need parens on single term
92 | if (groupNode.HasParens
93 | && groupNode.Left is TermNode leftTermNode
94 | && groupNode.Right == null)
95 | {
96 |
97 | if (groupNode.IsNegated.HasValue && groupNode.IsNegated.Value)
98 | {
99 | groupNode.HasParens = false;
100 | }
101 | else
102 | {
103 | groupNode.ReplaceSelf(leftTermNode);
104 | node = leftTermNode;
105 | }
106 | }
107 |
108 | // don't need parens on single term
109 | if (groupNode.HasParens
110 | && groupNode.Right is TermNode rightTermNode
111 | && groupNode.Left == null)
112 | {
113 |
114 | if (groupNode.IsNegated.HasValue && groupNode.IsNegated.Value)
115 | {
116 | groupNode.HasParens = false;
117 | }
118 | else
119 | {
120 | groupNode.ReplaceSelf(rightTermNode);
121 | node = rightTermNode;
122 | }
123 | }
124 | }
125 |
126 | return node;
127 | }
128 |
129 | public static async Task RunAsync(IQueryNode node, IQueryVisitorContext context = null)
130 | {
131 | var result = await new CleanupQueryVisitor().AcceptAsync(node, context);
132 | return result.ToString();
133 | }
134 |
135 | public static string Run(IQueryNode node, IQueryVisitorContext context = null)
136 | {
137 | return RunAsync(node, context).GetAwaiter().GetResult();
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/DebugQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System.CodeDom.Compiler;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 | using Foundatio.Parsers.LuceneQueries.Extensions;
6 | using Foundatio.Parsers.LuceneQueries.Nodes;
7 |
8 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
9 |
10 | public class DebugQueryVisitor : QueryNodeVisitorWithResultBase
11 | {
12 | private readonly StringBuilder _builder = new();
13 | private readonly IndentedTextWriter _writer;
14 |
15 | public DebugQueryVisitor()
16 | {
17 | _writer = new IndentedTextWriter(new StringWriter(_builder));
18 | }
19 |
20 | public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
21 | {
22 | await _writer.WriteLineAsync("Group:").ConfigureAwait(false);
23 | _writer.Indent++;
24 | _writer.WriteLineIf(node.IsNegated.HasValue, "IsNegated: {0}", node.IsNegated);
25 | _writer.WriteLineIf(node.Field != null, "Field: {0}", node.Field);
26 | _writer.WriteLineIf(node.GetOriginalField() != null, "Original Field: {0}", node.GetOriginalField());
27 | _writer.WriteLineIf(node.Prefix != null, "Prefix: {0}", node.Prefix);
28 | _writer.WriteLineIf(node.Boost != null, "Boost: {0}", node.Boost);
29 | _writer.WriteLineIf(node.Proximity != null, "Proximity: {0}", node.Proximity);
30 |
31 | _writer.WriteIf(node.Left != null, "Left - ");
32 | if (node.Left != null)
33 | await node.Left.AcceptAsync(this, context).ConfigureAwait(false);
34 |
35 | _writer.WriteIf(node.Right != null, "Right - ");
36 | if (node.Right != null)
37 | await node.Right.AcceptAsync(this, context).ConfigureAwait(false);
38 |
39 | _writer.WriteLineIf(node.Operator != GroupOperator.Default, "Operator: {0}", node.Operator);
40 | _writer.WriteLineIf(node.HasParens, "Parens: true");
41 |
42 | WriteData(node);
43 |
44 | _writer.Indent--;
45 | }
46 |
47 | public override void Visit(TermNode node, IQueryVisitorContext context)
48 | {
49 | _writer.WriteLine("Term: ");
50 | _writer.Indent++;
51 | _writer.WriteLineIf(node.Field != null, "Field: {0}", node.Field);
52 | _writer.WriteLineIf(node.GetOriginalField() != null, "Original Field: {0}", node.GetOriginalField());
53 | _writer.WriteLineIf(node.IsNegated.HasValue, "IsNegated: {0}", node.IsNegated);
54 | _writer.WriteLineIf(node.Prefix != null, "Prefix: {0}", node.Prefix);
55 | _writer.WriteLine("IsQuoted: {0}", node.IsQuotedTerm);
56 | _writer.WriteLine("IsRegex: {0}", node.IsRegexTerm);
57 | _writer.WriteLineIf(node.Term != null, "Term: {0}", node.Term);
58 | _writer.WriteLineIf(node.Boost != null, "Boost: {0}", node.Boost);
59 | _writer.WriteLineIf(node.Proximity != null, "Proximity: {0}", node.Proximity);
60 |
61 | WriteData(node);
62 |
63 | _writer.Indent--;
64 | }
65 |
66 | public override void Visit(TermRangeNode node, IQueryVisitorContext context)
67 | {
68 | _writer.WriteLine("Term Range: ");
69 | _writer.Indent++;
70 | _writer.WriteLineIf(node.Field != null, "Field: {0}", node.Field);
71 | _writer.WriteLineIf(node.GetOriginalField() != null, "Original Field: {0}", node.GetOriginalField());
72 | _writer.WriteLineIf(node.IsNegated.HasValue, "IsNegated: {0}", node.IsNegated);
73 | _writer.WriteLineIf(node.Prefix != null, "Prefix: {0}", node.Prefix);
74 | _writer.WriteLineIf(node.Operator != null, "Operator: {0}", node.Operator);
75 | _writer.WriteLineIf(node.Max != null, "Max: {0}", node.Max);
76 | _writer.WriteLineIf(node.Min != null, "Min: {0}", node.Min);
77 | _writer.WriteLineIf(node.MinInclusive.HasValue, "MinInclusive: {0}", node.MinInclusive);
78 | _writer.WriteLineIf(node.MaxInclusive.HasValue, "MaxInclusive: {0}", node.MaxInclusive);
79 |
80 | WriteData(node);
81 |
82 | _writer.Indent--;
83 | }
84 |
85 | public override void Visit(ExistsNode node, IQueryVisitorContext context)
86 | {
87 | _writer.WriteLine("Exists: ");
88 | _writer.Indent++;
89 | _writer.WriteLineIf(node.Field != null, "Field: {0}", node.Field);
90 | _writer.WriteLineIf(node.GetOriginalField() != null, "Original Field: {0}", node.GetOriginalField());
91 | _writer.WriteLineIf(node.IsNegated.HasValue, "IsNegated: {0}", node.IsNegated);
92 | _writer.WriteLineIf(node.Prefix != null, "Prefix: {0}", node.Prefix);
93 |
94 | WriteData(node);
95 |
96 | _writer.Indent--;
97 | }
98 |
99 | public override void Visit(MissingNode node, IQueryVisitorContext context)
100 | {
101 | _writer.WriteLine("Missing: ");
102 | _writer.Indent++;
103 | _writer.WriteLineIf(node.Field != null, "Field: {0}", node.Field);
104 | _writer.WriteLineIf(node.GetOriginalField() != null, "Original Field: {0}", node.GetOriginalField());
105 | _writer.WriteLineIf(node.IsNegated.HasValue, "IsNegated: {0}", node.IsNegated);
106 | _writer.WriteLineIf(node.Prefix != null, "Prefix: {0}", node.Prefix);
107 |
108 | WriteData(node);
109 |
110 | _writer.Indent--;
111 | }
112 |
113 | private void WriteData(QueryNodeBase node)
114 | {
115 | if (node.Data.Count <= 0)
116 | return;
117 |
118 | _writer.WriteLine("Data:");
119 | _writer.Indent++;
120 | foreach (var kvp in node.Data)
121 | {
122 | _writer.Write(kvp.Key);
123 | if (kvp.Value != null)
124 | {
125 | _writer.Write(": ");
126 | _writer.WriteLine(kvp.Value.ToString());
127 | }
128 | }
129 | _writer.Indent--;
130 | }
131 |
132 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
133 | {
134 | await node.AcceptAsync(this, context);
135 | return _builder.ToString();
136 | }
137 |
138 | public static Task RunAsync(IQueryNode node, IQueryVisitorContext context = null)
139 | {
140 | return new DebugQueryVisitor().AcceptAsync(node, context);
141 | }
142 |
143 | public static string Run(IQueryNode node, IQueryVisitorContext context = null)
144 | {
145 | return RunAsync(node, context).GetAwaiter().GetResult();
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/FieldResolverQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
8 |
9 | public class FieldResolverQueryVisitor : ChainableQueryVisitor
10 | {
11 | private readonly QueryFieldResolver _globalResolver;
12 |
13 | public FieldResolverQueryVisitor(QueryFieldResolver globalResolver = null)
14 | {
15 | _globalResolver = globalResolver;
16 | }
17 |
18 | public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
19 | {
20 | await ResolveField(node, context);
21 | await base.VisitAsync(node, context);
22 | }
23 |
24 | public override Task VisitAsync(TermNode node, IQueryVisitorContext context)
25 | {
26 | return ResolveField(node, context);
27 | }
28 |
29 | public override Task VisitAsync(TermRangeNode node, IQueryVisitorContext context)
30 | {
31 | return ResolveField(node, context);
32 | }
33 |
34 | public override Task VisitAsync(ExistsNode node, IQueryVisitorContext context)
35 | {
36 | return ResolveField(node, context);
37 | }
38 |
39 | public override Task VisitAsync(MissingNode node, IQueryVisitorContext context)
40 | {
41 | return ResolveField(node, context);
42 | }
43 |
44 | private async Task ResolveField(IFieldQueryNode node, IQueryVisitorContext context)
45 | {
46 | if (node.Parent == null || node.Field == null)
47 | return;
48 |
49 | var contextResolver = context.GetFieldResolver();
50 | if (_globalResolver == null && contextResolver == null)
51 | return;
52 |
53 | try
54 | {
55 | string resolvedField = null;
56 | if (contextResolver != null)
57 | resolvedField = await contextResolver(node.Field, context).ConfigureAwait(false);
58 | if (resolvedField == null && _globalResolver != null)
59 | resolvedField = await _globalResolver(node.Field, context).ConfigureAwait(false);
60 |
61 | if (resolvedField == null)
62 | {
63 | if (context.QueryType is QueryTypes.Aggregation && node.Field.StartsWith("@"))
64 | return;
65 |
66 | // add field to unresolved fields list
67 | context.GetValidationResult().UnresolvedFields.Add(node.Field);
68 | return;
69 | }
70 |
71 | if (!resolvedField.Equals(node.Field))
72 | {
73 | node.SetOriginalField(node.Field);
74 | node.Field = resolvedField;
75 | }
76 | }
77 | catch (Exception ex)
78 | {
79 | context.AddValidationError($"Error in field resolver callback when resolving field ({node.Field}): {ex.Message}");
80 | }
81 | }
82 |
83 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
84 | {
85 | await node.AcceptAsync(this, context).ConfigureAwait(false);
86 | return node;
87 | }
88 |
89 | public static Task RunAsync(IQueryNode node, QueryFieldResolver resolver, IQueryVisitorContextWithFieldResolver context = null)
90 | {
91 | context ??= new QueryVisitorContext();
92 | context.SetFieldResolver(resolver);
93 | return new FieldResolverQueryVisitor().AcceptAsync(node, context);
94 | }
95 |
96 | public static Task RunAsync(IQueryNode node, Func resolver, IQueryVisitorContextWithFieldResolver context = null)
97 | {
98 | context ??= new QueryVisitorContext();
99 | context.SetFieldResolver((field, _) => Task.FromResult(resolver(field)));
100 | return new FieldResolverQueryVisitor().AcceptAsync(node, context);
101 | }
102 |
103 | public static IQueryNode Run(IQueryNode node, QueryFieldResolver resolver, IQueryVisitorContextWithFieldResolver context = null)
104 | {
105 | return RunAsync(node, resolver, context).GetAwaiter().GetResult();
106 | }
107 |
108 | public static IQueryNode Run(IQueryNode node, Func resolver, IQueryVisitorContextWithFieldResolver context = null)
109 | {
110 | return RunAsync(node, resolver, context).GetAwaiter().GetResult();
111 | }
112 |
113 | public static Task RunAsync(IQueryNode node, IDictionary map, IQueryVisitorContextWithFieldResolver context = null)
114 | {
115 | context ??= new QueryVisitorContext();
116 | context.SetFieldResolver(map.ToHierarchicalFieldResolver());
117 | return new FieldResolverQueryVisitor().AcceptAsync(node, context);
118 | }
119 |
120 | public static IQueryNode Run(IQueryNode node, IDictionary map, IQueryVisitorContextWithFieldResolver context = null)
121 | {
122 | return RunAsync(node, map, context).GetAwaiter().GetResult();
123 | }
124 | }
125 |
126 | public delegate Task QueryFieldResolver(string field, IQueryVisitorContext context);
127 |
128 | public class FieldMap : Dictionary { }
129 |
130 | public static class FieldMapExtensions
131 | {
132 | public static string GetValueOrNull(this IDictionary map, string field)
133 | {
134 | if (map == null || field == null)
135 | return null;
136 |
137 | if (map.TryGetValue(field, out string value))
138 | return value;
139 |
140 | return null;
141 | }
142 |
143 | public static QueryFieldResolver ToHierarchicalFieldResolver(this IDictionary map, string resultPrefix = null)
144 | {
145 | return (field, _) =>
146 | {
147 | if (field == null)
148 | return null;
149 |
150 | if (map.TryGetValue(field, out string result))
151 | return Task.FromResult($"{resultPrefix}{result}");
152 |
153 | // start at the longest path and go backwards until we find a match in the map
154 | int currentPart = field.LastIndexOf('.');
155 | while (currentPart > 0)
156 | {
157 | string currentName = field.Substring(0, currentPart);
158 | if (map.TryGetValue(currentName, out string currentResult))
159 | return Task.FromResult($"{resultPrefix}{currentResult}{field.Substring(currentPart)}");
160 |
161 | currentPart = field.LastIndexOf('.', currentPart - 1);
162 | }
163 |
164 | return Task.FromResult(field);
165 | };
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/GenerateQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Nodes;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
6 |
7 | public class GenerateQueryVisitor : QueryNodeVisitorWithResultBase
8 | {
9 | private readonly StringBuilder _builder = new();
10 |
11 | public override Task VisitAsync(GroupNode node, IQueryVisitorContext context)
12 | {
13 | _builder.Append(node.ToString(context != null ? context.DefaultOperator : GroupOperator.Default));
14 |
15 | return Task.CompletedTask;
16 | }
17 |
18 | public override void Visit(TermNode node, IQueryVisitorContext context)
19 | {
20 | _builder.Append(node);
21 | }
22 |
23 | public override void Visit(TermRangeNode node, IQueryVisitorContext context)
24 | {
25 | _builder.Append(node);
26 | }
27 |
28 | public override void Visit(ExistsNode node, IQueryVisitorContext context)
29 | {
30 | _builder.Append(node);
31 | }
32 |
33 | public override void Visit(MissingNode node, IQueryVisitorContext context)
34 | {
35 | _builder.Append(node);
36 | }
37 |
38 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
39 | {
40 | await node.AcceptAsync(this, context).ConfigureAwait(false);
41 | return _builder.ToString();
42 | }
43 |
44 | public static Task RunAsync(IQueryNode node, IQueryVisitorContext context = null)
45 | {
46 | return new GenerateQueryVisitor().AcceptAsync(node, context);
47 | }
48 |
49 | public static string Run(IQueryNode node, IQueryVisitorContext context = null)
50 | {
51 | return RunAsync(node, context).GetAwaiter().GetResult();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/GetReferencedFieldsQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
8 |
9 | [Obsolete("Use QueryNodeExtensions.GetReferencedFields() extension method instead.")]
10 | public class GetReferencedFieldsQueryVisitor : QueryNodeVisitorWithResultBase>
11 | {
12 | public override Task> AcceptAsync(IQueryNode node, IQueryVisitorContext context)
13 | {
14 | return Task.FromResult(node.GetReferencedFields(context));
15 | }
16 |
17 | public static Task> RunAsync(IQueryNode node, IQueryVisitorContext context = null)
18 | {
19 | return new GetReferencedFieldsQueryVisitor().AcceptAsync(node, context);
20 | }
21 |
22 | public static ISet Run(IQueryNode node, IQueryVisitorContext context = null)
23 | {
24 | return RunAsync(node, context).GetAwaiter().GetResult();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IChainableQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using Foundatio.Parsers.LuceneQueries.Nodes;
2 |
3 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
4 |
5 | public interface IChainableQueryVisitor : IQueryNodeVisitorWithResult { }
6 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IQueryNodeVisitor.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 |
4 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | public interface IQueryNodeVisitor
7 | {
8 | Task VisitAsync(IQueryNode node, IQueryVisitorContext context);
9 | }
10 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IQueryNodeVisitorWithResult.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 |
4 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | public interface IQueryNodeVisitorWithResult : IQueryNodeVisitor
7 | {
8 | Task AcceptAsync(IQueryNode node, IQueryVisitorContext context);
9 | }
10 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IQueryVisitorContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 |
4 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | public interface IQueryVisitorContext
7 | {
8 | GroupOperator DefaultOperator { get; set; }
9 | string[] DefaultFields { get; set; }
10 | string QueryType { get; set; }
11 | IDictionary Data { get; }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IQueryVisitorContextWithFieldResolver.cs:
--------------------------------------------------------------------------------
1 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
2 |
3 | public interface IQueryVisitorContextWithFieldResolver : IQueryVisitorContext
4 | {
5 | QueryFieldResolver FieldResolver { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IQueryVisitorContextWithIncludeResolver.cs:
--------------------------------------------------------------------------------
1 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
2 |
3 | public interface IQueryVisitorContextWithIncludeResolver : IQueryVisitorContext
4 | {
5 | IncludeResolver IncludeResolver { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IQueryVisitorContextWithValidation.cs:
--------------------------------------------------------------------------------
1 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
2 |
3 | public interface IQueryVisitorContextWithValidation : IQueryVisitorContext
4 | {
5 | QueryValidationOptions ValidationOptions { get; set; }
6 | QueryValidationResult ValidationResult { get; set; }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/IncludeVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Extensions;
5 | using Foundatio.Parsers.LuceneQueries.Nodes;
6 |
7 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
8 |
9 | public delegate bool ShouldSkipIncludeFunc(TermNode node, IQueryVisitorContext context);
10 | public delegate Task IncludeResolver(string name);
11 |
12 | public class IncludeVisitor : ChainableMutatingQueryVisitor
13 | {
14 | private readonly LuceneQueryParser _parser = new();
15 | private readonly ShouldSkipIncludeFunc _shouldSkipInclude;
16 | private readonly string _includeName;
17 |
18 | public IncludeVisitor(ShouldSkipIncludeFunc shouldSkipInclude = null, string includeName = "include")
19 | {
20 | _shouldSkipInclude = shouldSkipInclude;
21 | _includeName = includeName;
22 | }
23 |
24 | public override async Task VisitAsync(TermNode node, IQueryVisitorContext context)
25 | {
26 | if (node.Field != "@" + _includeName || (_shouldSkipInclude != null && _shouldSkipInclude(node, context)))
27 | return node;
28 |
29 | var includeResolver = context.GetIncludeResolver();
30 | if (includeResolver == null)
31 | return node;
32 |
33 | var includes = context.GetValidationResult().ReferencedIncludes;
34 | includes.Add(node.Term);
35 |
36 | var includeStack = context.GetIncludeStack();
37 | if (includeStack.Contains(node.Term))
38 | {
39 | context.AddValidationError($"Recursive {_includeName} ({node.Term})");
40 | return node;
41 | }
42 |
43 | try
44 | {
45 | string includedQuery = await includeResolver(node.Term).ConfigureAwait(false);
46 | if (includedQuery == null)
47 | {
48 | context.AddValidationError($"Unresolved {_includeName} ({node.Term})");
49 | context.GetValidationResult().UnresolvedIncludes.Add(node.Term);
50 | }
51 |
52 | if (String.IsNullOrEmpty(includedQuery))
53 | return node;
54 |
55 | includeStack.Push(node.Term);
56 |
57 | var result = (GroupNode)await _parser.ParseAsync(includedQuery).ConfigureAwait(false);
58 | result.HasParens = true;
59 | await VisitAsync(result, context).ConfigureAwait(false);
60 |
61 | includeStack.Pop();
62 |
63 | return node.ReplaceSelf(result);
64 | }
65 | catch (Exception ex)
66 | {
67 | context.AddValidationError($"Error in {_includeName} resolver callback when resolving {_includeName} ({node.Term}): {ex.Message}");
68 | context.GetValidationResult().UnresolvedIncludes.Add(node.Term);
69 |
70 | return node;
71 | }
72 | }
73 |
74 | public static Task RunAsync(IQueryNode node, IncludeResolver includeResolver, IQueryVisitorContextWithIncludeResolver context = null, ShouldSkipIncludeFunc shouldSkipInclude = null)
75 | {
76 | context ??= new QueryVisitorContext();
77 | context.SetIncludeResolver(includeResolver);
78 |
79 | return new IncludeVisitor(shouldSkipInclude).AcceptAsync(node, context);
80 | }
81 |
82 | public static IQueryNode Run(IQueryNode node, IncludeResolver includeResolver, IQueryVisitorContextWithIncludeResolver context = null, ShouldSkipIncludeFunc shouldSkipInclude = null)
83 | {
84 | return RunAsync(node, includeResolver, context, shouldSkipInclude).GetAwaiter().GetResult();
85 | }
86 |
87 | public static IQueryNode Run(IQueryNode node, Func includeResolver, IQueryVisitorContextWithIncludeResolver context = null, ShouldSkipIncludeFunc shouldSkipInclude = null)
88 | {
89 | return RunAsync(node, name => Task.FromResult(includeResolver(name)), context, shouldSkipInclude).GetAwaiter().GetResult();
90 | }
91 |
92 | public static Task RunAsync(IQueryNode node, IDictionary includes, IQueryVisitorContextWithIncludeResolver context = null, ShouldSkipIncludeFunc shouldSkipInclude = null)
93 | {
94 | return RunAsync(node, name => Task.FromResult(includes.ContainsKey(name) ? includes[name] : null), context, shouldSkipInclude);
95 | }
96 |
97 | public static IQueryNode Run(IQueryNode node, IDictionary includes, IQueryVisitorContextWithIncludeResolver context = null, ShouldSkipIncludeFunc shouldSkipInclude = null)
98 | {
99 | return RunAsync(node, includes, context, shouldSkipInclude).GetAwaiter().GetResult();
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/InvertQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Foundatio.Parsers.LuceneQueries.Extensions;
6 | using Foundatio.Parsers.LuceneQueries.Nodes;
7 |
8 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
9 |
10 | public class InvertQueryVisitor : ChainableMutatingQueryVisitor
11 | {
12 | public InvertQueryVisitor(IEnumerable nonInvertedFields = null)
13 | {
14 | NonInvertedFields.AddRange(nonInvertedFields);
15 | }
16 |
17 | public List NonInvertedFields { get; } = new List();
18 |
19 | public override Task VisitAsync(GroupNode node, IQueryVisitorContext context)
20 | {
21 | bool onlyNonInvertedFields = node.GetReferencedFields().All(f => NonInvertedFields.Contains(f, StringComparer.OrdinalIgnoreCase));
22 | bool hasNonInvertedFields = node.GetReferencedFields().Any(f => NonInvertedFields.Contains(f, StringComparer.OrdinalIgnoreCase));
23 | bool onlyInvertedFields = !hasNonInvertedFields;
24 |
25 | // don't invert groups that only contain non-inverted fields
26 | if (onlyNonInvertedFields)
27 | return Task.FromResult(node);
28 |
29 | if (onlyInvertedFields)
30 | {
31 | // invert, don't visit children
32 | node = node.InvertNegation() as GroupNode;
33 |
34 | var alternateInvertedCriteria = context.GetAlternateInvertedCriteria();
35 | if (alternateInvertedCriteria != null)
36 | node = node.ReplaceSelf(new GroupNode
37 | {
38 | Left = alternateInvertedCriteria,
39 | Right = node.Clone(),
40 | Operator = GroupOperator.Or,
41 | HasParens = true
42 | });
43 |
44 | return Task.FromResult(node);
45 | }
46 |
47 | // otherwise, continue visiting children and invert them
48 | return base.VisitAsync(node, context);
49 | }
50 |
51 | public override Task VisitAsync(IQueryNode node, IQueryVisitorContext context)
52 | {
53 | if (context.DefaultOperator == GroupOperator.Or)
54 | throw new ArgumentException("Queries using OR as the default operator can not be inverted.");
55 |
56 | if (node is GroupNode groupNode)
57 | return VisitAsync(groupNode, context);
58 |
59 | if (node is IFieldQueryNode fieldNode && NonInvertedFields.Contains(fieldNode.Field, StringComparer.OrdinalIgnoreCase))
60 | return Task.FromResult(node);
61 |
62 | node = node.InvertNegation();
63 |
64 | var alternateInvertedCriteria = context.GetAlternateInvertedCriteria();
65 | if (alternateInvertedCriteria != null)
66 | node = node.ReplaceSelf(new GroupNode
67 | {
68 | Left = alternateInvertedCriteria,
69 | Right = node.Clone(),
70 | Operator = GroupOperator.Or,
71 | HasParens = true
72 | });
73 |
74 | return Task.FromResult(node);
75 | }
76 |
77 | public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
78 | {
79 | return node.AcceptAsync(this, context);
80 | }
81 |
82 | public static async Task RunAsync(IQueryNode node, IEnumerable nonInvertedFields = null, IQueryVisitorContext context = null)
83 | {
84 | var result = await new InvertQueryVisitor(nonInvertedFields).AcceptAsync(node, context);
85 | return result.ToString();
86 | }
87 |
88 | public static string Run(IQueryNode node, IEnumerable nonInvertedFields = null, IQueryVisitorContext context = null)
89 | {
90 | return RunAsync(node, nonInvertedFields, context).GetAwaiter().GetResult();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/QueryNodeVisitorBase.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 |
4 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | public abstract class QueryNodeVisitorBase : IQueryNodeVisitor
7 | {
8 | public virtual async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
9 | {
10 | foreach (var child in node.Children)
11 | await VisitAsync(child, context).ConfigureAwait(false);
12 | }
13 |
14 | public virtual void Visit(TermNode node, IQueryVisitorContext context) { }
15 |
16 | public virtual Task VisitAsync(TermNode node, IQueryVisitorContext context)
17 | {
18 | Visit(node, context);
19 | return Task.CompletedTask;
20 | }
21 |
22 | public virtual void Visit(TermRangeNode node, IQueryVisitorContext context) { }
23 |
24 | public virtual Task VisitAsync(TermRangeNode node, IQueryVisitorContext context)
25 | {
26 | Visit(node, context);
27 | return Task.CompletedTask;
28 | }
29 |
30 | public virtual void Visit(ExistsNode node, IQueryVisitorContext context) { }
31 |
32 | public virtual Task VisitAsync(ExistsNode node, IQueryVisitorContext context)
33 | {
34 | Visit(node, context);
35 | return Task.CompletedTask;
36 | }
37 |
38 | public virtual void Visit(MissingNode node, IQueryVisitorContext context) { }
39 |
40 | public virtual Task VisitAsync(MissingNode node, IQueryVisitorContext context)
41 | {
42 | Visit(node, context);
43 | return Task.CompletedTask;
44 | }
45 |
46 | public virtual async Task VisitAsync(IQueryNode node, IQueryVisitorContext context)
47 | {
48 | if (node is GroupNode groupNode)
49 | {
50 | await VisitAsync(groupNode, context);
51 | return node;
52 | }
53 |
54 | if (node is TermNode termNode)
55 | {
56 | await VisitAsync(termNode, context);
57 | return node;
58 | }
59 |
60 | if (node is TermRangeNode termRangeNode)
61 | {
62 | await VisitAsync(termRangeNode, context);
63 | return node;
64 | }
65 |
66 | if (node is MissingNode missingNode)
67 | {
68 | await VisitAsync(missingNode, context);
69 | return node;
70 | }
71 |
72 | if (node is ExistsNode existsNode)
73 | {
74 | await VisitAsync(existsNode, context);
75 | return node;
76 | }
77 |
78 | return node;
79 | }
80 | }
81 |
82 | public abstract class MutatingQueryNodeVisitorBase : IQueryNodeVisitor
83 | {
84 | public virtual async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
85 | {
86 | foreach (var child in node.Children)
87 | await VisitAsync(child, context).ConfigureAwait(false);
88 |
89 | return node;
90 | }
91 |
92 | public virtual IQueryNode Visit(TermNode node, IQueryVisitorContext context) => node;
93 |
94 | public virtual Task VisitAsync(TermNode node, IQueryVisitorContext context)
95 | {
96 | return Task.FromResult(Visit(node, context));
97 | }
98 |
99 | public virtual IQueryNode Visit(TermRangeNode node, IQueryVisitorContext context) => node;
100 |
101 | public virtual Task VisitAsync(TermRangeNode node, IQueryVisitorContext context)
102 | {
103 | return Task.FromResult(Visit(node, context));
104 | }
105 |
106 | public virtual IQueryNode Visit(ExistsNode node, IQueryVisitorContext context) => node;
107 |
108 | public virtual Task VisitAsync(ExistsNode node, IQueryVisitorContext context)
109 | {
110 | return Task.FromResult(Visit(node, context));
111 | }
112 |
113 | public virtual IQueryNode Visit(MissingNode node, IQueryVisitorContext context) => node;
114 |
115 | public virtual Task VisitAsync(MissingNode node, IQueryVisitorContext context)
116 | {
117 | return Task.FromResult(Visit(node, context));
118 | }
119 |
120 | public virtual Task VisitAsync(IQueryNode node, IQueryVisitorContext context)
121 | {
122 | if (node is GroupNode groupNode)
123 | return VisitAsync(groupNode, context);
124 |
125 | if (node is TermNode termNode)
126 | return VisitAsync(termNode, context);
127 |
128 | if (node is TermRangeNode termRangeNode)
129 | return VisitAsync(termRangeNode, context);
130 |
131 | if (node is MissingNode missingNode)
132 | return VisitAsync(missingNode, context);
133 |
134 | if (node is ExistsNode existsNode)
135 | return VisitAsync(existsNode, context);
136 |
137 | return Task.FromResult(node);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/QueryNodeVisitorWithResultBase.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 |
4 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | public abstract class QueryNodeVisitorWithResultBase : QueryNodeVisitorBase, IQueryNodeVisitorWithResult
7 | {
8 | public abstract Task AcceptAsync(IQueryNode node, IQueryVisitorContext context);
9 | }
10 |
11 | public abstract class MutatingQueryNodeVisitorWithResultBase : MutatingQueryNodeVisitorBase, IQueryNodeVisitorWithResult
12 | {
13 | public abstract Task AcceptAsync(IQueryNode node, IQueryVisitorContext context);
14 | }
15 |
16 | public abstract class ChainableQueryVisitor : QueryNodeVisitorWithResultBase, IChainableQueryVisitor
17 | {
18 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
19 | {
20 | var result = await node.AcceptAsync(this, context).ConfigureAwait(false);
21 | return result;
22 | }
23 | }
24 |
25 | public abstract class ChainableMutatingQueryVisitor : MutatingQueryNodeVisitorWithResultBase, IChainableQueryVisitor
26 | {
27 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
28 | {
29 | var result = await node.AcceptAsync(this, context).ConfigureAwait(false);
30 | return result;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/QueryVisitorContext.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Foundatio.Parsers.LuceneQueries.Nodes;
3 |
4 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
5 |
6 | public class QueryVisitorContext : IQueryVisitorContext, IQueryVisitorContextWithFieldResolver, IQueryVisitorContextWithIncludeResolver, IQueryVisitorContextWithValidation
7 | {
8 | public GroupOperator DefaultOperator { get; set; } = GroupOperator.And;
9 | public string[] DefaultFields { get; set; }
10 | public string QueryType { get; set; } = QueryTypes.Query;
11 | public IDictionary Data { get; } = new Dictionary();
12 | public QueryFieldResolver FieldResolver { get; set; }
13 | public IncludeResolver IncludeResolver { get; set; }
14 | public QueryValidationOptions ValidationOptions { get; set; }
15 | public QueryValidationResult ValidationResult { get; set; }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/RemoveFieldsQueryVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Foundatio.Parsers.LuceneQueries.Extensions;
6 | using Foundatio.Parsers.LuceneQueries.Nodes;
7 |
8 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
9 |
10 | public class RemoveFieldsQueryVisitor : ChainableQueryVisitor
11 | {
12 | public RemoveFieldsQueryVisitor(IEnumerable fieldsToRemove)
13 | {
14 | if (fieldsToRemove == null)
15 | throw new ArgumentNullException(nameof(fieldsToRemove));
16 |
17 | string[] fieldsToRemoveList = fieldsToRemove.ToArray();
18 | ShouldRemoveField = f => fieldsToRemoveList.Contains(f, StringComparer.OrdinalIgnoreCase);
19 | }
20 |
21 | public RemoveFieldsQueryVisitor(Func shouldRemoveFieldFunc)
22 | {
23 | if (shouldRemoveFieldFunc == null)
24 | throw new ArgumentNullException(nameof(shouldRemoveFieldFunc));
25 |
26 | ShouldRemoveField = shouldRemoveFieldFunc;
27 | }
28 |
29 | public Func ShouldRemoveField { get; }
30 |
31 | public override Task VisitAsync(IQueryNode node, IQueryVisitorContext context)
32 | {
33 | if (node is IFieldQueryNode fieldNode && fieldNode.Field != null && ShouldRemoveField(fieldNode.Field))
34 | {
35 | node.RemoveSelf();
36 | return Task.FromResult(null);
37 | }
38 |
39 | return base.VisitAsync(node, context);
40 | }
41 |
42 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
43 | {
44 | await node.AcceptAsync(this, context);
45 | return node;
46 | }
47 |
48 | public static async Task RunAsync(IQueryNode node, IEnumerable nonInvertedFields = null, IQueryVisitorContext context = null)
49 | {
50 | var result = await new RemoveFieldsQueryVisitor(nonInvertedFields).AcceptAsync(node, context);
51 | return result.ToString();
52 | }
53 |
54 | public static async Task RunAsync(IQueryNode node, Func shouldRemoveFieldFunc, IQueryVisitorContext context = null)
55 | {
56 | var result = await new RemoveFieldsQueryVisitor(shouldRemoveFieldFunc).AcceptAsync(node, context);
57 | return result.ToString();
58 | }
59 |
60 | public static string Run(IQueryNode node, IEnumerable nonInvertedFields = null, IQueryVisitorContext context = null)
61 | {
62 | return RunAsync(node, nonInvertedFields, context).GetAwaiter().GetResult();
63 | }
64 |
65 | public static string Run(IQueryNode node, Func shouldRemoveFieldFunc, IQueryVisitorContext context = null)
66 | {
67 | return RunAsync(node, shouldRemoveFieldFunc, context).GetAwaiter().GetResult();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/TermToFieldVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Foundatio.Parsers.LuceneQueries.Nodes;
4 |
5 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
6 |
7 | public class TermToFieldVisitor : ChainableQueryVisitor
8 | {
9 | public override void Visit(TermNode node, IQueryVisitorContext context)
10 | {
11 | if (String.IsNullOrEmpty(node.Term) || (node.Field != null && node.Field.StartsWith("@")))
12 | return;
13 |
14 | node.Field = node.Term;
15 | node.Term = null;
16 | }
17 |
18 | public static Task RunAsync(IQueryNode node, IQueryVisitorContext context = null)
19 | {
20 | return new TermToFieldVisitor().AcceptAsync(node, context);
21 | }
22 |
23 | public static void Run(IQueryNode node, IQueryVisitorContext context = null)
24 | {
25 | RunAsync(node, context).GetAwaiter().GetResult();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.LuceneQueries/Visitors/VisitorWithPriority.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Diagnostics;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Nodes;
5 |
6 | namespace Foundatio.Parsers.LuceneQueries.Visitors;
7 |
8 | [DebuggerDisplay("{Priority}, {Visitor}")]
9 | public class QueryVisitorWithPriority : IChainableQueryVisitor
10 | {
11 | public int Priority { get; set; }
12 | public IQueryNodeVisitorWithResult Visitor { get; set; }
13 |
14 | public Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
15 | {
16 | return Visitor.AcceptAsync(node, context);
17 | }
18 |
19 | public Task VisitAsync(IQueryNode node, IQueryVisitorContext context)
20 | {
21 | return Visitor.VisitAsync(node, context);
22 | }
23 |
24 | public class PriorityComparer : IComparer
25 | {
26 | public int Compare(QueryVisitorWithPriority x, QueryVisitorWithPriority y)
27 | {
28 | return x.Priority.CompareTo(y.Priority);
29 | }
30 |
31 | public static readonly PriorityComparer Instance = new();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.SqlQueries/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Foundatio.Parsers.SqlQueries.Extensions;
4 |
5 | internal static class EnumerableExtensions
6 | {
7 | public delegate void ElementAction(T element, ElementInfo info);
8 |
9 | public static void ForEach(this IEnumerable elements, ElementAction action)
10 | {
11 | using IEnumerator enumerator = elements.GetEnumerator();
12 | bool isFirst = true;
13 | bool hasNext = enumerator.MoveNext();
14 | int index = 0;
15 |
16 | while (hasNext)
17 | {
18 | T current = enumerator.Current;
19 | hasNext = enumerator.MoveNext();
20 | action(current, new ElementInfo(index, isFirst, !hasNext));
21 | isFirst = false;
22 | index++;
23 | }
24 | }
25 |
26 | public struct ElementInfo
27 | {
28 | public ElementInfo(int index, bool isFirst, bool isLast)
29 | : this()
30 | {
31 | Index = index;
32 | IsFirst = isFirst;
33 | IsLast = isLast;
34 | }
35 |
36 | public int Index { get; private set; }
37 | public bool IsFirst { get; private set; }
38 | public bool IsLast { get; private set; }
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.SqlQueries/Extensions/TypeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Foundatio.Parsers.SqlQueries.Extensions;
5 |
6 | public static class TypeExtensions
7 | {
8 | private static readonly IList _integerTypes = new List()
9 | {
10 | typeof (byte),
11 | typeof (short),
12 | typeof (int),
13 | typeof (long),
14 | typeof (sbyte),
15 | typeof (ushort),
16 | typeof (uint),
17 | typeof (ulong),
18 | typeof (byte?),
19 | typeof (short?),
20 | typeof (int?),
21 | typeof (long?),
22 | typeof (sbyte?),
23 | typeof (ushort?),
24 | typeof (uint?),
25 | typeof (ulong?)
26 | };
27 |
28 | public static Type UnwrapNullable(this Type type)
29 | {
30 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
31 | return Nullable.GetUnderlyingType(type);
32 |
33 | return type;
34 | }
35 |
36 | public static bool IsString(this Type type) => type == typeof(string);
37 | public static bool IsDateTime(this Type typeToCheck) => typeToCheck == typeof(DateTime) || typeToCheck == typeof(DateTime?);
38 | public static bool IsDateOnly(this Type typeToCheck) => typeToCheck == typeof(DateOnly) || typeToCheck == typeof(DateOnly?);
39 | public static bool IsBoolean(this Type typeToCheck) => typeToCheck == typeof(bool) || typeToCheck == typeof(bool?);
40 | public static bool IsNumeric(this Type type) => type.IsFloatingPoint() || type.IsIntegerBased();
41 | public static bool IsIntegerBased(this Type type) => _integerTypes.Contains(type);
42 | public static bool IsFloatingPoint(this Type type) => type == typeof(decimal) || type == typeof(float) || type == typeof(double);
43 | }
44 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.SqlQueries/Foundatio.Parsers.SqlQueries.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.SqlQueries/Visitors/GenerateSqlVisitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries.Nodes;
5 | using Foundatio.Parsers.LuceneQueries.Visitors;
6 | using Foundatio.Parsers.SqlQueries.Extensions;
7 |
8 | namespace Foundatio.Parsers.SqlQueries.Visitors;
9 |
10 | public class GenerateSqlVisitor : QueryNodeVisitorWithResultBase
11 | {
12 | private readonly StringBuilder _builder = new();
13 |
14 | public override Task VisitAsync(GroupNode node, IQueryVisitorContext context)
15 | {
16 | if (context is not ISqlQueryVisitorContext sqlContext)
17 | throw new InvalidOperationException("The context must be an ISqlQueryVisitorContext.");
18 |
19 | _builder.Append(node.ToDynamicLinqString(sqlContext));
20 |
21 | return Task.CompletedTask;
22 | }
23 |
24 | public override void Visit(TermNode node, IQueryVisitorContext context)
25 | {
26 | if (context is not ISqlQueryVisitorContext sqlContext)
27 | throw new InvalidOperationException("The context must be an ISqlQueryVisitorContext.");
28 |
29 | _builder.Append(node.ToDynamicLinqString(sqlContext));
30 | }
31 |
32 | public override void Visit(TermRangeNode node, IQueryVisitorContext context)
33 | {
34 | if (context is not ISqlQueryVisitorContext sqlContext)
35 | throw new InvalidOperationException("The context must be an ISqlQueryVisitorContext.");
36 |
37 | _builder.Append(node.ToDynamicLinqString(sqlContext));
38 | }
39 |
40 | public override void Visit(ExistsNode node, IQueryVisitorContext context)
41 | {
42 | if (context is not ISqlQueryVisitorContext sqlContext)
43 | throw new InvalidOperationException("The context must be an ISqlQueryVisitorContext.");
44 |
45 | _builder.Append(node.ToDynamicLinqString(sqlContext));
46 | }
47 |
48 | public override void Visit(MissingNode node, IQueryVisitorContext context)
49 | {
50 | if (context is not ISqlQueryVisitorContext sqlContext)
51 | throw new InvalidOperationException("The context must be an ISqlQueryVisitorContext.");
52 |
53 | _builder.Append(node.ToDynamicLinqString(sqlContext));
54 | }
55 |
56 | public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext context)
57 | {
58 | await node.AcceptAsync(this, context).ConfigureAwait(false);
59 | return _builder.ToString();
60 | }
61 |
62 | public static Task RunAsync(IQueryNode node, IQueryVisitorContext context = null)
63 | {
64 | return new GenerateSqlVisitor().AcceptAsync(node, context);
65 | }
66 |
67 | public static string Run(IQueryNode node, IQueryVisitorContext context = null)
68 | {
69 | return RunAsync(node, context).GetAwaiter().GetResult();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.SqlQueries/Visitors/ISqlQueryVisitorContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Foundatio.Parsers.LuceneQueries.Visitors;
4 |
5 | namespace Foundatio.Parsers.SqlQueries.Visitors;
6 |
7 | public interface ISqlQueryVisitorContext : IQueryVisitorContext
8 | {
9 | List Fields { get; set; }
10 | Action SearchTokenizer { get; set; }
11 | Func DateTimeParser { get; set; }
12 | Func DateOnlyParser { get; set; }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Foundatio.Parsers.SqlQueries/Visitors/SqlQueryVisitorContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Text;
5 | using Foundatio.Parsers.LuceneQueries.Visitors;
6 | using Microsoft.EntityFrameworkCore.Metadata;
7 |
8 | namespace Foundatio.Parsers.SqlQueries.Visitors;
9 |
10 | public class SqlQueryVisitorContext : QueryVisitorContext, ISqlQueryVisitorContext
11 | {
12 | public List Fields { get; set; }
13 | public Action SearchTokenizer { get; set; } = static _ => { };
14 | public Func DateTimeParser { get; set; }
15 | public Func DateOnlyParser { get; set; }
16 | public IEntityType EntityType { get; set; }
17 | }
18 |
19 | [DebuggerDisplay("{FullName} IsNumber: {IsNumber} IsMoney: {IsMoney} IsDate: {IsDate} IsBoolean: {IsBoolean} IsCollection: {IsCollection}")]
20 | public class EntityFieldInfo
21 | {
22 | public required string Name { get; init; }
23 | public required string FullName { get; init; }
24 | public bool IsNumber { get; set; }
25 | public bool IsMoney { get; set; }
26 | public bool IsDate { get; set; }
27 | public bool IsDateOnly { get; set; }
28 | public bool IsBoolean { get; set; }
29 | public bool IsCollection { get; set; }
30 | public bool IsNavigation { get; set; }
31 | public EntityFieldInfo Parent { get; set; }
32 | public IDictionary Data { get; set; } = new Dictionary();
33 |
34 | protected bool Equals(EntityFieldInfo other) => Name == other.Name;
35 |
36 | public override bool Equals(object obj)
37 | {
38 | if (obj is null)
39 | {
40 | return false;
41 | }
42 |
43 | if (ReferenceEquals(this, obj))
44 | {
45 | return true;
46 | }
47 |
48 | if (obj.GetType() != GetType())
49 | {
50 | return false;
51 | }
52 |
53 | return Equals((EntityFieldInfo)obj);
54 | }
55 |
56 | public override int GetHashCode() => (Name != null ? Name.GetHashCode() : 0);
57 |
58 | public (string fieldPrefix, string fieldSuffix) GetFieldPrefixAndSuffix()
59 | {
60 | var fieldTree = new List();
61 | EntityFieldInfo current = Parent;
62 | while (current != null)
63 | {
64 | fieldTree.Add(current);
65 | current = current.Parent;
66 | }
67 |
68 | fieldTree.Reverse();
69 |
70 | var prefix = new StringBuilder();
71 | var suffix = new StringBuilder();
72 | foreach (var field in fieldTree)
73 | {
74 | if (field.IsCollection)
75 | {
76 | prefix.Append($"{field.Name}.Any(");
77 | suffix.Append(")");
78 | }
79 | else
80 | {
81 | prefix.Append(field.Name).Append(".");
82 | }
83 | };
84 |
85 | return (prefix.ToString(), suffix.ToString());
86 | }
87 | }
88 |
89 | public class SearchTerm
90 | {
91 | public EntityFieldInfo FieldInfo { get; set; }
92 | public string Term { get; set; }
93 | public List Tokens { get; set; }
94 | public SqlSearchOperator Operator { get; set; } = SqlSearchOperator.Contains;
95 | }
96 |
97 | public enum SqlSearchOperator { Equals, Contains, StartsWith }
98 |
--------------------------------------------------------------------------------
/tests/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;
5 | False
6 | $(NoWarn);CS1591;NU1701
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticMappingResolverTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 | using Nest;
4 | using Xunit;
5 | using Xunit.Abstractions;
6 |
7 | namespace Foundatio.Parsers.ElasticQueries.Tests;
8 |
9 | public class ElasticMappingResolverTests : ElasticsearchTestBase
10 | {
11 | public ElasticMappingResolverTests(ITestOutputHelper output, ElasticsearchFixture fixture) : base(output, fixture)
12 | {
13 | Log.DefaultMinimumLevel = Microsoft.Extensions.Logging.LogLevel.Trace;
14 | }
15 |
16 | private ITypeMapping MapMyNestedType(TypeMappingDescriptor m)
17 | {
18 | return m
19 | .AutoMap()
20 | .Dynamic()
21 | .DynamicTemplates(t => t.DynamicTemplate("idx_text", t => t.Match("text*").Mapping(m => m.Text(mp => mp.AddKeywordAndSortFields()))))
22 | .Properties(p => p
23 | .Text(p1 => p1.Name(n => n.Field1).AddKeywordAndSortFields())
24 | .Text(p1 => p1.Name(n => n.Field4).AddKeywordAndSortFields())
25 | .FieldAlias(a => a.Path(n => n.Field4).Name("field4alias"))
26 | .Text(p1 => p1.Name(n => n.Field5).AddKeywordAndSortFields(true))
27 | );
28 | }
29 |
30 | [Fact]
31 | public void CanResolveCodedProperty()
32 | {
33 | string index = CreateRandomIndex(MapMyNestedType);
34 |
35 | Client.IndexMany([
36 | new MyNestedType
37 | {
38 | Field1 = "value1",
39 | Field2 = "value2",
40 | Nested =
41 | [
42 | new MyType
43 | {
44 | Field1 = "banana",
45 | Data = {
46 | { "number-0001", 23 },
47 | { "text-0001", "Hey" },
48 | { "spaced field", "hey" }
49 | }
50 | }
51 | ]
52 | },
53 | new MyNestedType { Field1 = "value2", Field2 = "value2" },
54 | new MyNestedType { Field1 = "value1", Field2 = "value4" }
55 | ], index);
56 | Client.Indices.Refresh(index);
57 |
58 | var resolver = ElasticMappingResolver.Create(MapMyNestedType, Client, index, _logger);
59 |
60 | var payloadProperty = resolver.GetMappingProperty("payload");
61 | Assert.IsType(payloadProperty);
62 | Assert.NotNull(payloadProperty.Name);
63 | }
64 |
65 | [Fact]
66 | public void CanResolveProperties()
67 | {
68 | string index = CreateRandomIndex(MapMyNestedType);
69 |
70 | Client.IndexMany([
71 | new MyNestedType
72 | {
73 | Field1 = "value1",
74 | Field2 = "value2",
75 | Nested =
76 | [
77 | new MyType
78 | {
79 | Field1 = "banana",
80 | Data = {
81 | { "number-0001", 23 },
82 | { "text-0001", "Hey" },
83 | { "spaced field", "hey" }
84 | }
85 | }
86 | ]
87 | },
88 | new MyNestedType { Field1 = "value2", Field2 = "value2" },
89 | new MyNestedType { Field1 = "value1", Field2 = "value4" }
90 | ], index);
91 | Client.Indices.Refresh(index);
92 |
93 | var resolver = ElasticMappingResolver.Create(MapMyNestedType, Client, index, _logger);
94 |
95 | string dynamicTextAggregation = resolver.GetAggregationsFieldName("nested.data.text-0001");
96 | Assert.Equal("nested.data.text-0001.keyword", dynamicTextAggregation);
97 |
98 | string dynamicSpacedAggregation = resolver.GetAggregationsFieldName("nested.data.spaced field");
99 | Assert.Equal("nested.data.spaced field.keyword", dynamicSpacedAggregation);
100 |
101 | string dynamicSpacedSort = resolver.GetSortFieldName("nested.data.spaced field");
102 | Assert.Equal("nested.data.spaced field.keyword", dynamicSpacedSort);
103 |
104 | string dynamicSpacedField = resolver.GetResolvedField("nested.data.spaced field");
105 | Assert.Equal("nested.data.spaced field", dynamicSpacedField);
106 |
107 | var field1Property = resolver.GetMappingProperty("Field1");
108 | Assert.IsType(field1Property);
109 |
110 | string field5Property = resolver.GetAggregationsFieldName("Field5");
111 | Assert.Equal("field5.keyword", field5Property);
112 |
113 | var unknownProperty = resolver.GetMappingProperty("UnknowN.test.doesNotExist");
114 | Assert.Null(unknownProperty);
115 |
116 | string field1 = resolver.GetResolvedField("FielD1");
117 | Assert.Equal("field1", field1);
118 |
119 | string emptyField = resolver.GetResolvedField(" ");
120 | Assert.Equal(" ", emptyField);
121 |
122 | string unknownField = resolver.GetResolvedField("UnknowN.test.doesNotExist");
123 | Assert.Equal("UnknowN.test.doesNotExist", unknownField);
124 |
125 | string unknownField2 = resolver.GetResolvedField("unknown.test.doesnotexist");
126 | Assert.Equal("unknown.test.doesnotexist", unknownField2);
127 |
128 | string unknownField3 = resolver.GetResolvedField("unknown");
129 | Assert.Equal("unknown", unknownField3);
130 |
131 | var field4Property = resolver.GetMappingProperty("Field4");
132 | Assert.IsType(field4Property);
133 |
134 | var field4ReflectionProperty = resolver.GetMappingProperty(new Field(typeof(MyNestedType).GetProperty("Field4")));
135 | Assert.IsType(field4ReflectionProperty);
136 |
137 | var field4ExpressionProperty = resolver.GetMappingProperty(new Field(GetObjectPath(p => p.Field4)));
138 | Assert.IsType(field4ExpressionProperty);
139 |
140 | var field4AliasMapping = resolver.GetMapping("Field4Alias", true);
141 | Assert.IsType(field4AliasMapping.Property);
142 | Assert.Same(field4Property, field4AliasMapping.Property);
143 |
144 | string field4sort = resolver.GetSortFieldName("Field4Alias");
145 | Assert.Equal("field4.sort", field4sort);
146 |
147 | string field4aggs = resolver.GetAggregationsFieldName("Field4Alias");
148 | Assert.Equal("field4.keyword", field4aggs);
149 |
150 | var nestedIdProperty = resolver.GetMappingProperty("Nested.Id");
151 | Assert.IsType(nestedIdProperty);
152 |
153 | string nestedId = resolver.GetResolvedField("Nested.Id");
154 | Assert.Equal("nested.id", nestedId);
155 |
156 | nestedIdProperty = resolver.GetMappingProperty("nested.id");
157 | Assert.IsType(nestedIdProperty);
158 |
159 | var nestedField1Property = resolver.GetMappingProperty("Nested.Field1");
160 | Assert.IsType(nestedField1Property);
161 |
162 | nestedField1Property = resolver.GetMappingProperty("nEsted.fieLD1");
163 | Assert.IsType(nestedField1Property);
164 |
165 | var nestedField2Property = resolver.GetMappingProperty("Nested.Field4");
166 | Assert.IsType(nestedField2Property);
167 |
168 | var nestedField5Property = resolver.GetMappingProperty("Nested.Field5");
169 | Assert.IsType(nestedField5Property);
170 |
171 | var nestedDataProperty = resolver.GetMappingProperty("Nested.Data");
172 | Assert.IsType(nestedDataProperty);
173 | }
174 |
175 | private static Expression GetObjectPath(Expression> objectPath)
176 | {
177 | return objectPath;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/tests/Foundatio.Parsers.ElasticQueries.Tests/Foundatio.Parsers.ElasticQueries.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | docker-compose.yml
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/Foundatio.Parsers.ElasticQueries.Tests/InvertQueryTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Foundatio.Parsers.LuceneQueries;
5 | using Foundatio.Parsers.LuceneQueries.Extensions;
6 | using Foundatio.Parsers.LuceneQueries.Nodes;
7 | using Foundatio.Parsers.LuceneQueries.Visitors;
8 | using Microsoft.Extensions.Logging;
9 | using Nest;
10 | using Xunit;
11 | using Xunit.Abstractions;
12 |
13 | namespace Foundatio.Parsers.ElasticQueries.Tests;
14 |
15 | public class InvertQueryTests : ElasticsearchTestBase
16 | {
17 | public const string OrgId = "1";
18 | public const string AltOrgId = "2";
19 |
20 | public InvertQueryTests(ITestOutputHelper output, SampleDataFixture fixture) : base(output, fixture) { }
21 |
22 | [Fact]
23 | public Task CanInvertTermQuery()
24 | {
25 | return InvertAndValidateQuery("deleted", "(NOT deleted)", null, true);
26 | }
27 |
28 | [Fact]
29 | public Task CanInvertFieldQuery()
30 | {
31 | return InvertAndValidateQuery("status:open", "(NOT status:open)", null, true);
32 | }
33 |
34 | [Fact]
35 | public Task CanInvertNotFieldQuery()
36 | {
37 | return InvertAndValidateQuery("NOT status:open", "status:open", null, true);
38 | }
39 |
40 | [Fact]
41 | public Task CanInvertMultipleTermsQuery()
42 | {
43 | return InvertAndValidateQuery("field1:value field2:value field3:value", "(NOT (field1:value field2:value field3:value))", null, true);
44 | }
45 |
46 | [Fact]
47 | public Task CanInvertOrGroupQuery()
48 | {
49 | return InvertAndValidateQuery("(field1:value OR field2:value)", "(NOT (field1:value OR field2:value))", null, true);
50 | }
51 |
52 | [Fact]
53 | public Task CanInvertFieldWithNonInvertedFieldQuery()
54 | {
55 | return InvertAndValidateQuery("field:value organizationId:value", "(NOT field:value) organizationId:value", null, true);
56 | }
57 |
58 | [Fact]
59 | public Task CanInvertAlternateCriteria()
60 | {
61 | return InvertAndValidateQuery("value", "(is_deleted:true OR (NOT value))", "is_deleted:true", true);
62 | }
63 |
64 | [Fact]
65 | public Task CanInvertAlternateCriteriaAndNonInvertedField()
66 | {
67 | return InvertAndValidateQuery("organizationId:value field1:value", "organizationId:value (is_deleted:true OR (NOT field1:value))", "is_deleted:true", true);
68 | }
69 |
70 | [Fact]
71 | public Task CanInvertNonInvertedFieldAndOrGroup()
72 | {
73 | return InvertAndValidateQuery("organizationId:value (field1:value OR field2:value)", "organizationId:value (NOT (field1:value OR field2:value))", null, true);
74 | }
75 |
76 | [Fact]
77 | public Task CanInvertAlternateCriteriaAndNonInvertedFieldAndOrGroup()
78 | {
79 | return InvertAndValidateQuery("organizationId:value (field1:value OR field2:value)", "organizationId:value (is_deleted:true OR (NOT (field1:value OR field2:value)))", "is_deleted:true", true);
80 | }
81 |
82 | [Fact]
83 | public Task CanInvertGroupNonInvertedField()
84 | {
85 | return InvertAndValidateQuery("(field1:value organizationId:value) field2:value", "((NOT field1:value) organizationId:value) (NOT field2:value)", null, true);
86 | }
87 |
88 | private async Task InvertAndValidateQuery(string query, string expected, string alternateInvertedCriteria, bool isValid)
89 | {
90 | var parser = new LuceneQueryParser();
91 |
92 | IQueryNode result;
93 | try
94 | {
95 | result = await parser.ParseAsync(query);
96 | }
97 | catch (FormatException ex)
98 | {
99 | Assert.False(isValid, ex.Message);
100 | return;
101 | }
102 |
103 | var invertQueryVisitor = new InvertQueryVisitor(["organizationId"]);
104 | var context = new QueryVisitorContext();
105 |
106 | if (!String.IsNullOrWhiteSpace(alternateInvertedCriteria))
107 | {
108 | var invertedAlternate = await parser.ParseAsync(alternateInvertedCriteria);
109 | context.SetAlternateInvertedCriteria(invertedAlternate);
110 | }
111 |
112 | result = await invertQueryVisitor.AcceptAsync(result, context);
113 | string invertedQuery = result.ToString();
114 | string nodes = await DebugQueryVisitor.RunAsync(result);
115 | _logger.LogInformation("{Result}", nodes);
116 | Assert.Equal(expected, invertedQuery);
117 |
118 | var total = await Client.CountAsync();
119 | var results = await Client.SearchAsync(s => s.QueryOnQueryString(query).TrackTotalHits(true));
120 | var invertedResults = await Client.SearchAsync(s => s.QueryOnQueryString(invertedQuery).TrackTotalHits(true));
121 |
122 | Assert.Equal(total.Count, results.Total + invertedResults.Total);
123 | }
124 | }
125 |
126 | public class InvertTest
127 | {
128 | public const string OrgId = "1";
129 | public const string AltOrgId = "2";
130 |
131 | public string Id { get; set; }
132 | public string OrganizationId { get; set; }
133 | public string Description { get; set; }
134 | public string Status { get; set; } = "open";
135 | public bool IsDeleted { get; set; }
136 | }
137 |
138 | public class SampleDataFixture : ElasticsearchFixture
139 | {
140 | public override async Task InitializeAsync()
141 | {
142 | await base.InitializeAsync();
143 |
144 | const string indexName = "test_invert";
145 | CreateNamedIndex(indexName, m => m
146 | .Properties(p => p
147 | .Keyword(p1 => p1.Name(n => n.Id))
148 | .Keyword(p1 => p1.Name(n => n.OrganizationId))
149 | .Text(p1 => p1.Name(n => n.Description))
150 | .Keyword(p1 => p1.Name(n => n.Status))
151 | .Boolean(p1 => p1.Name(n => n.IsDeleted))
152 | ));
153 |
154 | var records = new List();
155 | int id = 1;
156 |
157 | for (int i = 0; i < 10000; i++, id++)
158 | {
159 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.OrgId, Description = $"Description {i}", Status = "open", IsDeleted = false });
160 | }
161 | for (int i = 0; i < 1000; i++, id++)
162 | {
163 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.OrgId, Description = $"Deleted Description {i}", Status = "open", IsDeleted = true });
164 | }
165 | for (int i = 0; i < 100; i++, id++)
166 | {
167 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.OrgId, Description = $"Regressed Description {i}", Status = "regressed", IsDeleted = false });
168 | }
169 | for (int i = 0; i < 100; i++, id++)
170 | {
171 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.OrgId, Description = $"Ignored Description {i}", Status = "ignored", IsDeleted = false });
172 | }
173 |
174 | for (int i = 0; i < 10000; i++, id++)
175 | {
176 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.AltOrgId, Description = $"Alt Description {i}", Status = "open", IsDeleted = false });
177 | }
178 | for (int i = 0; i < 1000; i++, id++)
179 | {
180 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.AltOrgId, Description = $"Deleted Alt Description {i}", Status = "open", IsDeleted = true });
181 | }
182 | for (int i = 0; i < 100; i++, id++)
183 | {
184 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.AltOrgId, Description = $"Regressed Alt Description {i}", Status = "regressed", IsDeleted = false });
185 | }
186 | for (int i = 0; i < 100; i++, id++)
187 | {
188 | records.Add(new InvertTest { Id = id.ToString(), OrganizationId = InvertTest.AltOrgId, Description = $"Ignored Alt Description {i}", Status = "ignored", IsDeleted = false });
189 | }
190 |
191 | await Client.IndexManyAsync(records, indexName);
192 |
193 | await Client.Indices.RefreshAsync(indexName);
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/tests/Foundatio.Parsers.ElasticQueries.Tests/Utility/ElasticsearchTestBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Foundatio.Parsers.ElasticQueries.Extensions;
6 | using Foundatio.Xunit;
7 | using Microsoft.Extensions.Logging;
8 | using Nest;
9 | using Xunit;
10 | using Xunit.Abstractions;
11 |
12 | namespace Foundatio.Parsers.ElasticQueries.Tests;
13 |
14 | public abstract class ElasticsearchTestBase : ElasticsearchTestBase
15 | {
16 | protected ElasticsearchTestBase(ITestOutputHelper output, ElasticsearchFixture fixture) : base(output, fixture) { }
17 | }
18 |
19 | public abstract class ElasticsearchTestBase : TestWithLoggingBase, IAsyncLifetime, IClassFixture where T : ElasticsearchFixture
20 | {
21 | private readonly T _fixture;
22 |
23 | public ElasticsearchTestBase(ITestOutputHelper output, T fixture) : base(output)
24 | {
25 | _fixture = fixture;
26 | _fixture.Log = Log;
27 | }
28 |
29 | protected IElasticClient Client => _fixture.Client;
30 |
31 | protected void CreateNamedIndex(string index, Func