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

Query DSL Nâng cao với .NET

Multi-Match Query

Tìm kiếm trong nhiều fields cùng lúc.

public async Task<List<Product>> MultiFieldSearchAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .MultiMatch(mm => mm
                .Query(keyword)
                .Fields(new[]
                {
                    "name^3",          // Boost name x3
                    "description^1",
                    "category^2",
                    "tags^1.5"
                })
                .Type(TextQueryType.BestFields) // Lấy field match tốt nhất
                // .Type(TextQueryType.MostFields)  // Cộng score tất cả fields
                // .Type(TextQueryType.CrossFields)  // Tìm terms trải qua nhiều fields
                .MinimumShouldMatch("75%") // Ít nhất 75% terms phải match
            )
        )
    );

    return response.Documents.ToList();
}

Fuzzy Query - Tìm kiếm mờ

Tìm kiếm chịu được lỗi chính tả.

public async Task<List<Product>> FuzzySearchAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Fuzzy(f => f
                .Field(field => field.Name)
                .Value(keyword)
                .Fuzziness(new Fuzziness("AUTO")) // AUTO: 0 cho 1-2 chars, 1 cho 3-5, 2 cho 5+
                .MaxExpansions(50)                 // Giới hạn số biến thể
                .PrefixLength(2)                   // Prefix không được fuzzy
            )
        )
    );

    return response.Documents.ToList();
}

// Trong Match query cũng có fuzziness
var response = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Match(m => m
            .Field(f => f.Name)
            .Query("iphoone") // Gõ sai
            .Fuzziness(new Fuzziness("AUTO"))
        )
    )
);

Wildcard & Prefix

// Wildcard: * = nhiều ký tự, ? = một ký tự (cẩn thận hiệu suất)
var wildcardResponse = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Wildcard(w => w
            .Field(f => f.Name)
            .Value("iphone*") // Bắt đầu bằng "iphone"
        )
    )
);

// Prefix: tìm documents bắt đầu bằng prefix (nhanh hơn wildcard)
var prefixResponse = await _es.SearchAsync<Product>(s => s
    .Query(q => q
        .Prefix(p => p
            .Field(f => f.Category)
            .Value("smart")  // "smartphones", "smartwatch", v.v.
        )
    )
);

Nested Query

Dùng khi field có type nested để giữ đúng quan hệ.

public class Product
{
    // ...
    public List<ProductReview> Reviews { get; set; } = [];
}

public class ProductReview
{
    public string UserId { get; set; } = string.Empty;
    public int Score { get; set; }
    public string Comment { get; set; } = string.Empty;
}

// Tìm products có review của Alice với score >= 4
public async Task<List<Product>> GetHighlyRatedByUserAsync(string userId, int minScore)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Nested(n => n
                .Path("reviews")                  // Đường dẫn đến nested field
                .Query(nq => nq
                    .Bool(b => b
                        .Must(
                            m => m.Term(t => t.Field("reviews.user_id").Value(userId)),
                            m => m.Range(r => r.NumberRange(nr => nr
                                .Field("reviews.score")
                                .Gte(minScore)
                            ))
                        )
                    )
                )
                .ScoreMode(ChildScoreMode.Max) // Lấy score cao nhất từ nested docs
            )
        )
    );

    return response.Documents.ToList();
}

Function Score Query - Tùy chỉnh Score

// Tăng score cho products in-stock và giá thấp
public async Task<List<Product>> SearchWithBoostingAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .FunctionScore(fs => fs
                .Query(fq => fq
                    .Match(m => m.Field(f => f.Name).Query(keyword))
                )
                .Functions(
                    // Boost x2 nếu in stock
                    new FunctionScoreContainer
                    {
                        Filter = new TermQuery("in_stock") { Value = true },
                        Weight = 2.0f
                    },
                    // Giảm score theo giá (giá thấp hơn = score cao hơn)
                    new FunctionScoreContainer
                    {
                        FieldValueFactor = new FieldValueFactorScoreFunction
                        {
                            Field = "stock_quantity",
                            Factor = 1.2f,
                            Modifier = FieldValueFactorModifier.Log1P,
                            Missing = 1
                        }
                    }
                )
                .ScoreMode(FunctionScoreMode.Multiply)
                .BoostMode(FunctionBoostMode.Sum)
            )
        )
    );

    return response.Documents.ToList();
}

Search Template

Templates tái sử dụng được, store trên server.

// Lưu template
await _es.PutScriptAsync("product-search-template", ps => ps
    .Script(sc => sc
        .Lang("mustache")
        .Source("""
        {
          "query": {
            "bool": {
              "must": [
                {{#query}}
                { "match": { "name": "{{query}}" } }
                {{/query}}
              ],
              "filter": [
                {{#category}}
                { "term": { "category": "{{category}}" } }
                {{/category}}
              ]
            }
          },
          "size": "{{size}}",
          "from": "{{from}}"
        }
        """)
    )
);

// Sử dụng template
var response = await _es.SearchTemplateAsync<Product>(s => s
    .Index("products")
    .Id("product-search-template")
    .Params(p => p
        .Add("query", "iphone")
        .Add("category", "smartphones")
        .Add("size", 20)
        .Add("from", 0)
    )
);

Highlight - Đánh dấu từ khóa

public async Task<List<SearchHitDto>> SearchWithHighlightAsync(string keyword)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Query(q => q
            .Match(m => m.Field(f => f.Name).Query(keyword))
        )
        .Highlight(h => h
            .Fields(
                hf => hf.Field(f => f.Name)
                    .NumberOfFragments(0)              // 0 = trả về toàn bộ field
                    .PreTags("<mark>")
                    .PostTags("</mark>"),
                hf => hf.Field(f => f.Description)
                    .NumberOfFragments(3)              // Lấy 3 đoạn chứa keyword
                    .FragmentSize(150)
                    .PreTags("<em>")
                    .PostTags("</em>")
            )
        )
    );

    return response.Hits.Select(hit => new SearchHitDto
    {
        Product = hit.Source!,
        NameHighlight = hit.Highlight?.GetValueOrDefault("name")?.FirstOrDefault(),
        DescriptionSnippets = hit.Highlight?.GetValueOrDefault("description")?.ToList() ?? [],
        Score = hit.Score ?? 0,
    }).ToList();
}

public record SearchHitDto
{
    public Product Product { get; init; } = default!;
    public string? NameHighlight { get; init; }
    public List<string> DescriptionSnippets { get; init; } = [];
    public double Score { get; init; }
}

Scroll API - Xuất dữ liệu lớn

Dùng khi cần duyệt qua số lượng lớn documents (> 10,000).

// Không dùng Scroll cho user-facing search - chỉ dùng cho export/processing
public async IAsyncEnumerable<Product> ScrollAllAsync(
    string index,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    var response = await _es.SearchAsync<Product>(s => s
        .Index(index)
        .Query(q => q.MatchAll())
        .Size(1000)
        .Scroll(new Duration("2m")) // Giữ scroll context 2 phút
    , ct);

    while (response.Documents.Any())
    {
        foreach (var doc in response.Documents)
            yield return doc;

        response = await _es.ScrollAsync<Product>(scroll => scroll
            .ScrollId(response.ScrollId!)
            .Scroll(new Duration("2m"))
        , ct);
    }

    // Xóa scroll context khi xong
    await _es.ClearScrollAsync(cs => cs.ScrollId(response.ScrollId!));
}

// Sử dụng
await foreach (var product in ScrollAllAsync("products"))
{
    await ProcessProductAsync(product);
}