برای رفع این دو مشکل میتوانیم از امکانات Query نویسی در RavenDb استفاده کنیم. به دلیل ذخیره سازی (ظاهرا) فلهای اطلاعات در NoSqlها، Query گرفتن از حجم بسیار زیاد این اطلاعات، کار زمان بری است و اجرای Query بدون Index گذاری، کار بیهودهای میشود. به همین دلیل با هر Query که اجرا میشود، به صورت خودکار یک Index برای آن توسط RavenDb ایجاد شده و Query بر روی Index ایجاد شده، اجرا میشود. عملیات Index کردن اطلاعات بصورت اتوماتیک در اولین بار اجرای Query با توجه به حجم دادهها میتواند بسیار کند باشد. همچنین ما کنترلی بر روی مدیریت ایندکسهای ایجاد شده نداریم.
Queryها در RavenDb به چند صورت نوشته میشوند:
Query
متد Query برای ایجاد Query با استفاده از Linq کاربرد دارد. به مثال زیر توجه کنید:
List<User> users = await _documentSession
.Query<Users>()
.Where(u => u.PhoneNumber.StartsWith("915"))
.ToListAsync();
اجرای Query بالا ابتدا باعث ایجاد یک Index بر روی ویژگی PhoneNumber میشود و سپس لیست کاربران را بر میگرداند.
برای بازیابی اطلاعات کاربران یک برنامه میتوانیم از Dictionary خود Query بگیریم:
var users = await _documentSession.Query<AppUser>()
.Where(u => u.Id.Equals("915"))
.Select(u => new
{
u.Apps[appCode].FirstName,
u.Apps [appCode].LastName,
})
.ToListAsync();
این Query در RQL که زبان پرس و جوی مخصوص RavenDb است، چیزی شبیه کد زیر میشود:
from Users as user
where startsWith(user.PhoneNumber, "915")
select {
FirstName : user.Apps ["59"].FirstName,
LastName : user.Apps ["59"].LastName
}
مشکلی که در این Query وجود دارد ایناست که کاربرانی که شماره تماس آنها با 915 شروع شده است ولی در برنامهای با کد 59 ثبت نشدهاند هم در Query بازگشت داده میشوند و مقادیر بازگشتی برای فیلدها هم null خواهد بود. اگر بجای ذکر صریح عبارت u. Apps [appCode].FirstName به صورت زیر عمل کنیم:
from u in _documentSession.Query<User>()
where u.PhoneNumber.StartsWith("915")
let app = u.Apps["59"]
select new
{
app.FirstName,
app.LastName,
};
عبارت
let app = u.Apps["59"] در RQL تبدیل به یک متد جاوااسکریپتی میشود و به کدی شبیه به کد زیر میرسیم:
declare function output(u) {
var app = u.Apps["59"];
return { FirstName : app.FirstName, LastName : app.LastName};
}
from Users as user
where startsWith(user.PhoneNumber, "915")
select output(user)
حالا میتوانیم Key مورد نظر در دیکشنری را هم در Query به شکل زیر دخیل کنیم:
app.FirstName,
app.LastName,
*key = u.ActiveInApps.Select(a => a.Key)
و در ادامه با استفاده از متد Search، این فیلد را که به کلید دیکشنری اشاره میکند، محدود کرده و بعد از آن Query خود را اجرا میکنیم:
query = query.Search(u => u.key, "59");
در صورتیکه بجای دیکشنری از آرایه استفاده کرده باشیم هم کدهای ما به همین صورت میباشد با کمی تغییرات مربوط به تفاوت List و Dictionary!
اما هنوز Query ما بدرستی کار نمیکند چرا که ویژگی Key در RavenDb ایندکس نشدهاست و نمیتواند این ایندکس را هم تشخیص دهد. دلیل آن هم این است که تنها ویژگیهایی که در مرتب سازی (Sort) و یا فیلتر مورد استفاده قرار گیرند، به ایندکسها اضافه میشوند. برای حل این مشکل باید بصورت دستی Index خود را در RavenDb بسازیم. این کار با ارث بری از کلاس پایهی AbstractIndexCreationTask شروع میشود و مدلی را که میخواهیم Index بر روی آن اعمال شود نیز ذکر میکنیم و بعد از آن در سازندهی کلاس، Index خود را میسازیم:
public class User_MyIndex : AbstractIndexCreationTask<User>
{
Map = users =>
from u in users
from app in u.Apps
select new
{
Id = u.Id,
PhoneNumber = u.PhoneNumber,
UserName = app.Value.UserName,
FirstName = app.Value.FirstName,
LastName = app.Value.LastName,
IsActive = app.Value.IsActive,
key = app.Key
};
}
در این ایندکس به ازای هر کاربر، تمام برنامههایی که ثبت شده، بررسی شده و ایندکس میشوند. نکتهای که باید به آن توجه کنید این است که ویژگیهای ذکر شده فقط به RavenDb نحوهی بازیابی فیلدهای سند را برای Index گذاری میگوید و همچنان خروجی این Index از نوع User بوده و تمام سند را بازگشت میدهد و باید از متد Select در صورت نیاز استفاده کنیم. برای اعمال این ایندکس به سمت سرور از متد:
new User_MyIndex().Execute(store);
و برای ارسال چندین Index به سمت سرور از متد:
IndexCreation.CreateIndexes(typeof(User_MyIndex).Assembly, store);
استفاده میکنیم. اکنون اگر به Query خود این ایندکس را معرفی کنیم، خروجی ما بهدرستی فقط کاربران برنامه مورد نظر را بر میگرداند:
from u in _documentSession.Query<User, User_MyIndex>() ...
کلاس AbstractIndexCreationTask متدهای زیادی برای کنترل دقیق Indexها در اختیار ما قرار میدهد که پرکاربردترین آنها میتوانند متدهای زیر باشند:
Index : نحوهی Index کردن هر یک از پراپرتیها را مشخص میکند.
Store : برای مواقعی کاربرد دارد که شما میخواهید مقدار Index شده را برای دسترسی سریعتر همرا با Index ذخیره کنید.
LoadDocument: این متد Id یا لیستی از Idها را به عنوان ورودی گرفته و سند مورد نظر را بازیابی میکند. زمانیکه میخواهیم اسناد مرتبط را همراه با سند، Index کنیم کاربرد دارد. برای مثال وقتی میخواهیم Addressهای کاربر را که در سندی جداگانه قرار دارند، به همراه اطلاعات او در Index شرکت دهیم:
select new
{
...
key = aia.Key,
Address = LoadDocument<Address>(aia.Value.AddressId),
// City = LoadDocument<Address>(aia.Value.AddressId).City,
};
و برای Indexکردن لیستی از اسناد مرتبط به صورت زیر از LoadDocument استفاده میکنیم:
Message = app.Messages.Select(m => LoadDocument<Message>(m).Content)
* زمانی که میخواهید کلید یک Dictionary را Index کنید و میخواهید نام فیلد آن را key قرار دهید باید از k کوچک استفاده کنید؛ چرا که Key، جزء کلمات رزرو شدهی RavenDb میباشد.
DocumentQuery
دسترسی بیشتری را بر روی Query ارسالی به سمت سرور به ما میدهد؛ اما strongly typed نیست. برای مثال Query بالا را به این صورت میتوانیم با DocumentQuery پیاده کنیم:
var users = _documentSession.Advanced.AsyncDocumentQuery<User, User_MyIndex>()
.WhereStartsWith(nameof(AppUser.PhoneNumber), "915")
.WhereEquals("key", appCode, exact: true)
.SelectFields<AppUserModel>(new[] { $"Apps[{appCode}].FirstName", $"Apps[{appCode}].LastName" })
.ToListAsync();
متدهای DocumentQuery بسیار متنوع هستند و میتوانید
لیست آنها را در اینجا مشاهده کنید.
MoreLikeThis (اسناد شبیه)
از رایجترین کارهایی که در وب سایتهای مطرح دیده میشود نمایش مطالب مرتبط با مطلب جاری میباشد و از آنجایی که RavenDb از Lucene.NET برای ایندکس کردن اسناد استفاده میکند، میتواند براحتی از MoreLikeThis موجود در پروژهی Contrib آن استفاده نماید.
مدل زیر را در نظر بگیرید:
public class Post
{
public int Id { get; set; }
public string Content { get; set; }
public string Title { get; set; }
public List<string> Tags { get; set; }
public string WriterName { get; set; }
public string WriterId { get; set; }
}
برای استفاده از MoreLikeThis باید ابتدا محتویات مطلب خود را با استفاده از StandardAnalyzer ایندکس گذاری کنیم. همانطور که گفته شد، برای Index کردن یک سند از کد زیر میتوانیم استفاده کنیم. با این تفاوت که نحوهی آنالیز سند را نیز مشخص میکنیم:
public class Post_ByContent : AbstractIndexCreationTask<Post>
{
public Post_ByContent()
{
Map = posts=> from post in posts
select new
{
post.Content
};
Analyzers.Add(p => p.Content, "StandardAnalyzer");
}
}
از این ایندکس در Query به همراه متد MoreLikeThis استفاده میکنیم: List<Post> posts = _documentSession
.Query<Post, Post_ByContent>()
.MoreLikeThis(builder => builder
.UsingDocument(p => p.Id == "posts/59-A")
.WithOptions(new MoreLikeThisOptions
{
Fields = new[] { nameof(Post.Content) },
StopWordsDocumentId = "appConfig/StopWords"
}))
.ToList();
ابتدا سندی را که میخواهیم اسناد شبیه به آن بازیابی شود، معرفی میکنیم. به اینصورت بررسی بر روی تمام فیلدهای Indexگذاری شده اعمال میشود. اگر بخواهیم تنظیماتی را به متد اضافه کنیم از MoreLikeThisOptions استفاده میکنیم. حداقل تنظیمات میتواند معرفی نام فیلد مورد نظر برای کاهش بار سرور و همچنین معرفی سندی که StopWordهای ما در آن قرار دارد، باشد. میتوانید در مورد
StopWordها و کاربرد آن در Lucene از این مقاله استفاده کنید.