---
url: /guide/elasticsearch.md
---
# Elasticsearch Integration

Foundatio.Lucene.Elasticsearch converts Lucene query strings to Elasticsearch Query DSL using the official Elastic.Clients.Elasticsearch 9.x client.

## Installation

```bash
dotnet add package Foundatio.Lucene.Elasticsearch
```

## Basic Usage

```csharp
using Foundatio.Lucene.Elasticsearch;
using Elastic.Clients.Elasticsearch;

var parser = new ElasticsearchQueryParser();

// Parse a Lucene query and convert to Elasticsearch Query DSL
var query = parser.BuildQuery("title:hello AND status:active");

// Use with the Elasticsearch client
var client = new ElasticsearchClient();
var response = await client.SearchAsync<Document>(s => s
    .Index("my-index")
    .Query(query)
);
```

## Configuration Options

The `ElasticsearchQueryParser` supports extensive configuration:

```csharp
var parser = new ElasticsearchQueryParser(config =>
{
    // Use scoring queries (match) vs filter queries (term)
    config.UseScoring = true;

    // Default fields for unfielded terms
    config.DefaultFields = ["title", "content", "description"];

    // Default boolean operator (AND or OR)
    config.DefaultOperator = BooleanOperator.And;

    // Field aliasing
    config.FieldMap = new FieldMap
    {
        { "author", "metadata.author" },
        { "created", "metadata.createdAt" },
        { "updated", "metadata.updatedAt" }
    };

    // Date field detection
    config.IsDateField = field =>
        field.EndsWith("date") ||
        field.EndsWith("timestamp") ||
        field == "created" ||
        field == "updated";

    // Default timezone for date ranges
    config.DefaultTimeZone = "America/Chicago";

    // Pre-resolved @include content (resolve saved queries from your store before building)
    config.Includes = new Dictionary<string, string>
    {
        ["active"] = "status:active AND deleted:false"
    };

    // Query validation
    config.ValidationOptions = new QueryValidationOptions
    {
        AllowLeadingWildcards = false
    };
});
```

## Query Types

### Term Queries

Simple field queries become term queries (or match queries if scoring is enabled):

```csharp
// Input
"status:active"

// Output (UseScoring = false)
{ "term": { "status": "active" } }

// Output (UseScoring = true)
{ "match": { "status": "active" } }
```

### Phrase Queries

Quoted phrases become match\_phrase queries:

```csharp
// Input
"title:\"hello world\""

// Output
{ "match_phrase": { "title": "hello world" } }
```

With proximity:

```csharp
// Input
"title:\"hello world\"~2"

// Output
{ "match_phrase": { "title": { "query": "hello world", "slop": 2 } } }
```

### Range Queries

Range syntax maps directly to Elasticsearch range queries:

```csharp
// Input
"price:[100 TO 500]"

// Output
{ "range": { "price": { "gte": 100, "lte": 500 } } }

// Input with exclusive boundaries
"price:{100 TO 500}"

// Output
{ "range": { "price": { "gt": 100, "lt": 500 } } }
```

### Boolean Queries

Boolean operators map to Elasticsearch bool queries:

```csharp
// Input
"title:hello AND status:active"

// Output
{
    "bool": {
        "must": [
            { "term": { "title": "hello" } },
            { "term": { "status": "active" } }
        ]
    }
}
```

### Wildcard Queries

Wildcards are converted to wildcard queries:

```csharp
// Input
"name:john*"

// Output
{ "wildcard": { "name": "john*" } }
```

### Regex Queries

Regex patterns become regexp queries:

```csharp
// Input
"name:/joh?n/"

// Output
{ "regexp": { "name": "joh?n" } }
```

### Exists/Missing Queries

Field existence checks:

```csharp
// Input
"_exists_:email"

// Output
{ "exists": { "field": "email" } }

// Input
"_missing_:phone"

// Output
{ "bool": { "must_not": { "exists": { "field": "phone" } } } }
```

## Geo Queries

> Geo query generation (`geo_distance` / `geo_bounding_box`) is **not supported in 1.0**. It is
> planned for a later release with a design that keeps the visitor pipeline synchronous (collect geo
> references, resolve coordinates outside the pipeline, then annotate the AST).

## Date Queries

Date fields with date math are automatically handled:

```csharp
config.IsDateField = field => field.EndsWith("date");
config.DefaultTimeZone = "America/Chicago";

// Input
"created:[now-7d TO now]"

// Output (date math evaluated)
{
    "range": {
        "created": {
            "gte": "2024-11-26T00:00:00",
            "lte": "2024-12-03T00:00:00",
            "time_zone": "America/Chicago"
        }
    }
}
```

## Error Handling

The parser throws `QueryParseException` for invalid queries:

```csharp
try
{
    var query = parser.BuildQuery("invalid::[query");
}
catch (QueryParseException ex)
{
    Console.WriteLine($"Parse error: {ex.Message}");
}
```

## Custom Visitors

Add custom visitors to transform the query before building:

```csharp
var parser = new ElasticsearchQueryParser();

// Add a custom visitor
parser.AddVisitor(new MyCustomVisitor());

var query = parser.BuildQuery("...");
```

## Complete Example

Here's a complete example with an ASP.NET Core API:

```csharp
[ApiController]
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
    private readonly ElasticsearchClient _client;
    private readonly ElasticsearchQueryParser _parser;

    public SearchController(ElasticsearchClient client)
    {
        _client = client;
        _parser = new ElasticsearchQueryParser(config =>
        {
            config.UseScoring = true;
            config.DefaultFields = ["title", "content"];
            config.FieldMap = new FieldMap
            {
                { "author", "metadata.author" },
                { "date", "metadata.publishedAt" }
            };
            config.IsDateField = f => f.Contains("date") || f.Contains("At");
            config.ValidationOptions = new QueryValidationOptions
            {
                AllowLeadingWildcards = false
            };
        });
    }

    [HttpGet]
    public async Task<IActionResult> Search(
        [FromQuery] string q,
        [FromQuery] int page = 1,
        [FromQuery] int size = 20)
    {
        try
        {
            var query = _parser.BuildQuery(q);

            var response = await _client.SearchAsync<Article>(s => s
                .Index("articles")
                .Query(query)
                .From((page - 1) * size)
                .Size(size)
                .Sort(so => so.Field("_score", f => f.Order(SortOrder.Desc)))
            );

            return Ok(new
            {
                Total = response.Total,
                Page = page,
                Results = response.Documents
            });
        }
        catch (QueryParseException ex)
        {
            return BadRequest(new { Error = ex.Message });
        }
    }
}
```

## Next Steps

* [Query Syntax](./query-syntax) - All supported query syntax
* [Visitors](./visitors) - Custom query transformation
* [Validation](./validation) - Query validation options
