feat: full text search

oneRain 2021-05-19 16:07:43 +08:00
parent ab60fb227b
commit e657a6ec69
4 changed files with 232 additions and 0 deletions

View File

@ -0,0 +1,39 @@
using NUnit.Framework;
using System.Threading.Tasks;
using LeanCloud.Storage;
namespace Storage.Test {
public class FullTextSearchTest : BaseTest {
[Test]
public async Task QueryByOrder() {
LCSearchQuery<Account> query = new LCSearchQuery<Account>("Account");
query.QueryString("*")
.OrderByDescending("balance")
.Limit(200);
LCSearchResponse<Account> response = await query.Find();
Assert.Greater(response.Hits, 0);
for (int i = 0; i < response.Results.Count - 1; i++) {
int b1 = response.Results[i].Balance;
int b2 = response.Results[i + 1].Balance;
Assert.GreaterOrEqual(b1, b2);
}
}
[Test]
public async Task QueryBySort() {
LCSearchQuery<Account> query = new LCSearchQuery<Account>("Account");
LCSearchSortBuilder sortBuilder = new LCSearchSortBuilder();
sortBuilder.OrderByAscending("balance");
query.QueryString("*")
.SortBy(sortBuilder)
.Limit(200);
LCSearchResponse<Account> response = await query.Find();
Assert.Greater(response.Hits, 0);
for (int i = 0; i < response.Results.Count - 1; i++) {
int b1 = response.Results[i].Balance;
int b2 = response.Results[i + 1].Balance;
Assert.LessOrEqual(b1, b2);
}
}
}
}

View File

@ -0,0 +1,125 @@
using System.Linq;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using LeanCloud.Common;
using LeanCloud.Storage.Internal.Object;
namespace LeanCloud.Storage {
public class LCSearchQuery<T> where T : LCObject {
private string className;
private string queryString;
private IEnumerable<string> highlights;
private IEnumerable<string> includeKeys;
private int limit;
private int skip;
private string sid;
private List<string> orders;
private LCSearchSortBuilder sortBuilder;
public LCSearchQuery(string className) {
this.className = className;
limit = 100;
skip = 0;
}
public LCSearchQuery<T> QueryString(string q) {
queryString = q;
return this;
}
public LCSearchQuery<T> Highlights(IEnumerable<string> highlights) {
this.highlights = highlights;
return this;
}
public LCSearchQuery<T> Include(IEnumerable<string> keys) {
includeKeys = keys;
return this;
}
public LCSearchQuery<T> Limit(int amount) {
limit = amount;
return this;
}
public LCSearchQuery<T> Skip(int amount) {
skip = amount;
return this;
}
public LCSearchQuery<T> Sid(string sid) {
this.sid = sid;
return this;
}
public LCSearchQuery<T> OrderByAscending(string key) {
orders = new List<string>();
orders.Add(key);
return this;
}
public LCSearchQuery<T> OrderByDescending(string key) {
return OrderByAscending($"-{key}");
}
public LCSearchQuery<T> AddAscendingOrder(string key) {
if (orders == null) {
orders = new List<string>();
}
orders.Add(key);
return this;
}
public LCSearchQuery<T> AddDescendingOrder(string key) {
return AddAscendingOrder($"-{key}");
}
public LCSearchQuery<T> SortBy(LCSearchSortBuilder sortBuilder) {
this.sortBuilder = sortBuilder;
return this;
}
public async Task<LCSearchResponse<T>> Find() {
string path = "search/select";
Dictionary<string, object> queryParams = new Dictionary<string, object> {
{ "clazz", className },
{ "limit", limit },
{ "skip", skip }
};
if (queryString != null) {
queryParams["q"] = queryString;
}
if (highlights != null && highlights.Count() > 0) {
queryParams["highlights"] = string.Join(",", highlights);
}
if (includeKeys != null && includeKeys.Count() > 0) {
queryParams["include"] = string.Join(",", includeKeys);
}
if (!string.IsNullOrEmpty(sid)) {
queryParams["sid"] = sid;
}
if (orders != null && orders.Count() > 0) {
queryParams["order"] = string.Join(",", orders);
}
if (sortBuilder != null) {
queryParams["sort"] = sortBuilder.Build();
}
Dictionary<string, object> response = await LCCore.HttpClient.Get<Dictionary<string, object>>(path, queryParams: queryParams);
LCSearchResponse<T> ret = new LCSearchResponse<T>();
ret.Hits = (int)response["hits"];
ret.Sid = (string)response["sid"];
List<object> results = response["results"] as List<object>;
List<T> list = new List<T>();
foreach (object data in results) {
LCObjectData objectData = LCObjectData.Decode(data as Dictionary<string, object>);
T obj = LCObject.Create(className) as T;
obj.Merge(objectData);
list.Add(obj);
}
ret.Results = list.AsReadOnly();
return ret;
}
}
}

View File

@ -0,0 +1,17 @@
using System.Collections.ObjectModel;
namespace LeanCloud.Storage {
public class LCSearchResponse<T> where T : LCObject {
public int Hits {
get; set;
}
public ReadOnlyCollection<T> Results {
get; set;
}
public string Sid {
get; set;
}
}
}

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
using LC.Newtonsoft.Json;
namespace LeanCloud.Storage {
public class LCSearchSortBuilder {
private List<object> fields;
public LCSearchSortBuilder() {
fields = new List<object>();
}
public LCSearchSortBuilder OrderByAscending(string key, string mode = null, string missing = null) {
return AddField(key, "asc", mode, missing);
}
public LCSearchSortBuilder OrderByDescending(string key, string mode = null, string missing = null) {
return AddField(key, "desc", mode, missing);
}
public LCSearchSortBuilder WhereNear(string key, LCGeoPoint point,
string order = null, string mode = null, string unit = null) {
fields.Add(new Dictionary<string, object> {
{ "_geo_distance", new Dictionary<string, object> {
{ key, new Dictionary<string, object> {
{ "lat", point.Latitude },
{ "lon", point.Longitude }
} },
{ "order", order ?? "asc" },
{ "mode", mode ?? "avg" },
{ "unit", unit ?? "km" }
} }
});
return this;
}
private LCSearchSortBuilder AddField(string key, string order = null, string mode = null, string missing = null) {
fields.Add(new Dictionary<string, object> {
{ key, new Dictionary<string, object> {
{ "order", order ?? "asc" },
{ "mode", mode ?? "avg" },
{ "missing", $"_{missing ?? "last"}" }
} }
});
return this;
}
internal string Build() {
return JsonConvert.SerializeObject(fields);
}
}
}