Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Basic Search với .NET

Cấu trúc Search Request

// Mọi search đều qua SearchAsync<T>
var response = await _es.SearchAsync<Product>(s => s
    .Index("products")
    .From(0)           // Offset (pagination)
    .Size(20)          // Số kết quả
    .Query(q => ...)   // Query DSL
    .Sort(so => ...)   // Sorting
    .Source(src => ...) // Chọn fields trả về
);

// Đọc kết quả
var products = response.Documents;          // IReadOnlyCollection<T>
var total    = response.Total;              // Tổng số documents match
var hits     = response.Hits;              // Kèm metadata (_score, _id, v.v.)
var maxScore = response.MaxScore;

// Tìm kiếm full-text trong một field
public async Task<List<Product>> SearchByNameAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Match(m => m
                .Field(f => f.Name)
                .Query(keyword)
                .Fuzziness(new Fuzziness(1)) // Cho phép 1 ký tự sai
                .Operator(Operator.And)      // Tất cả từ phải xuất hiện
            )
        )
        .Size(20)
    );

    return response.Documents.ToList();
}
// Equivalent JSON:
{
  "query": {
    "match": {
      "name": {
        "query": "iphone pro",
        "fuzziness": 1,
        "operator": "AND"
      }
    }
  }
}

Term Query - Exact Match

// Tìm chính xác - không phân tích (dành cho keyword fields)
public async Task<List<Product>> GetByCategoryAsync(string category)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Term(t => t
                .Field(f => f.Category)
                .Value(category)
            )
        )
    );

    return response.Documents.ToList();
}

// Terms - tìm trong danh sách giá trị
public async Task<List<Product>> GetByCategoriesAsync(IEnumerable<string> categories)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Terms(t => t
                .Field(f => f.Category)
                .Terms(new TermsQueryField(
                    categories.Select(c => FieldValue.String(c)).ToArray()
                ))
            )
        )
    );

    return response.Documents.ToList();
}

Range Query

public async Task<List<Product>> GetByPriceRangeAsync(
    decimal? minPrice,
    decimal? maxPrice)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Range(r => r
                .NumberRange(nr =>
                {
                    nr.Field(f => f.Price);
                    if (minPrice.HasValue) nr.Gte((double)minPrice.Value);
                    if (maxPrice.HasValue) nr.Lte((double)maxPrice.Value);
                    return nr;
                })
            )
        )
        .Sort(so => so.Field(f => f.Price, new FieldSort { Order = SortOrder.Asc }))
    );

    return response.Documents.ToList();
}

// Date range
public async Task<List<Product>> GetRecentProductsAsync(DateTime since)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Range(r => r
                .DateRange(dr => dr
                    .Field(f => f.CreatedAt)
                    .Gte(since)
                )
            )
        )
    );

    return response.Documents.ToList();
}

Bool Query - Kết hợp nhiều điều kiện

Bool query là query phổ biến nhất, kết hợp các queries khác:

must:     Điều kiện bắt buộc - ảnh hưởng relevance score
filter:   Điều kiện bắt buộc - KHÔNG ảnh hưởng score (nhanh hơn, được cache)
should:   Điều kiện tùy chọn - tăng score nếu match
must_not: Phủ định - loại bỏ documents match
public async Task<SearchResult<Product>> SearchProductsAsync(ProductSearchParams p)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Bool(b =>
            {
                // must: bắt buộc, ảnh hưởng score
                if (!string.IsNullOrEmpty(p.Keyword))
                {
                    b.Must(m => m
                        .MultiMatch(mm => mm
                            .Fields(Fields.FromExpressions<Product>(
                                f => f.Name,
                                f => f.Description
                            ))
                            .Query(p.Keyword)
                            .Type(TextQueryType.BestFields)
                        )
                    );
                }

                // filter: bắt buộc, KHÔNG ảnh hưởng score (nhanh hơn)
                var filters = new List<Action<QueryDescriptor<Product>>>();

                if (!string.IsNullOrEmpty(p.Category))
                    filters.Add(f => f.Term(t => t.Field(x => x.Category).Value(p.Category)));

                if (p.MinPrice.HasValue || p.MaxPrice.HasValue)
                    filters.Add(f => f.Range(r => r.NumberRange(nr =>
                    {
                        nr.Field(x => x.Price);
                        if (p.MinPrice.HasValue) nr.Gte((double)p.MinPrice.Value);
                        if (p.MaxPrice.HasValue) nr.Lte((double)p.MaxPrice.Value);
                        return nr;
                    })));

                if (p.InStockOnly)
                    filters.Add(f => f.Term(t => t.Field(x => x.InStock).Value(true)));

                if (filters.Count > 0)
                    b.Filter(filters.ToArray());

                // should: tùy chọn, tăng score
                if (p.PreferredBrands?.Any() == true)
                    b.Should(sh => sh
                        .Terms(t => t
                            .Field(f => f.Brand)
                            .Terms(new TermsQueryField(
                                p.PreferredBrands.Select(FieldValue.String).ToArray()
                            ))
                            .Boost(1.5f)
                        )
                    );

                return b;
            })
        )
        .From((p.Page - 1) * p.PageSize)
        .Size(p.PageSize)
        .Sort(so =>
        {
            switch (p.SortBy)
            {
                case "price_asc":
                    so.Field(f => f.Price, new FieldSort { Order = SortOrder.Asc });
                    break;
                case "price_desc":
                    so.Field(f => f.Price, new FieldSort { Order = SortOrder.Desc });
                    break;
                default: // relevance
                    so.Score(new ScoreSort { Order = SortOrder.Desc });
                    break;
            }
            return so;
        })
    );

    return new SearchResult<Product>
    {
        Items = response.Documents.ToList(),
        Total = response.Total,
        Page = p.Page,
        PageSize = p.PageSize,
    };
}

public record ProductSearchParams
{
    public string? Keyword { get; init; }
    public string? Category { get; init; }
    public decimal? MinPrice { get; init; }
    public decimal? MaxPrice { get; init; }
    public bool InStockOnly { get; init; }
    public List<string>? PreferredBrands { get; init; }
    public string SortBy { get; init; } = "relevance";
    public int Page { get; init; } = 1;
    public int PageSize { get; init; } = 20;
}

Sorting

var response = await _es.SearchAsync<Product>(s => s
    .Sort(so => so
        // Sort theo score trước
        .Score(new ScoreSort { Order = SortOrder.Desc })
        // Rồi theo price
        .Field(f => f.Price, new FieldSort { Order = SortOrder.Asc })
        // Sort theo text field phải dùng .keyword
        .Field("name.keyword", new FieldSort { Order = SortOrder.Asc })
    )
);

Fields Selection (Source filtering)

// Chỉ lấy fields cần thiết - giảm network traffic
var response = await _es.SearchAsync<Product>(s => s
    .Source(src => src
        .Includes(i => i.Fields(
            f => f.Name,
            f => f.Price,
            f => f.Category
        ))
        .Excludes(e => e.Fields(f => f.Description)) // Loại field nặng
    )
);

// Hoặc tắt _source hoàn toàn khi chỉ cần IDs
var response2 = await _es.SearchAsync<Product>(s => s
    .Source(false)
    .Query(q => q.MatchAll())
);
var ids = response2.Hits.Select(h => h.Id).ToList();

Handling Kết quả

var response = await _es.SearchAsync<Product>(s => s
    .Query(q => q.Match(m => m.Field(f => f.Name).Query("iphone")))
);

// Đọc documents
var products = response.Documents.ToList();

// Kèm metadata (score, id, highlights)
foreach (var hit in response.Hits)
{
    var product = hit.Source!;
    var score   = hit.Score;
    var id      = hit.Id;

    // Highlight snippets (nếu có config Highlight)
    if (hit.Highlight?.TryGetValue("name", out var highlights) == true)
    {
        var snippet = highlights.First();
        Console.WriteLine($"Highlight: {snippet}");
    }
}

// Tổng số kết quả
Console.WriteLine($"Total: {response.Total}, Returned: {response.Documents.Count}");