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;
Match Query - Full-text Search
// 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}");