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);
}