Последние пару дней я экспериментировал с ElasticSearch и различными клиентскими библиотеками для .NET. В этом посте я подробно расскажу о реализации индексирования и поиска с использованием ElasticSearch в .NET. Для получения подробной документации Elasticsearch посетите официальный сайт или прочитайте этот пост Джоэла Абрахамссона .
Я использую PlainElastic.Net в качестве клиента Elastic Search. PlainElastic — очень простая и легкая библиотека для Elasticsearch. Он использует обычный json для индексации и запросов, это дает мне больше свободы и инструментов для создания json из пользовательских данных для индексации и запросов.
Чтобы сделать его более гибким, наша система получает данные из базы данных с использованием представлений. Класс Datareader преобразует эти данные в динамические объекты. Затем преобразует его в json и передает его в PlainElastic для индексации. Динамический объект облегчает жизнь, так как мы можем использовать этот класс с разными взглядами, не беспокоясь о сильных типах. Ниже приведен класс Dynamic, который я создал.
public class ElasticEntity:DynamicObject
{
private Dictionary<string, object> _members = new Dictionary<string, object>();
public static ElasticEntity CreateFrom(Dictionary<string, object> members)
{
ElasticEntity entity = new ElasticEntity();
entity.SetMembers(members);
return entity;
}
public string GetValue(string property)
{
if (_members.ContainsKey(property))
{
object tmp= _members[property];
if (tmp == null)
return string.Empty;
else
return Convert.ToString(tmp);
}
return string.Empty;
}
public Dictionary<string, object> GetDictionary()
{
return _members;
}
internal void SetMembers(Dictionary<string, object> members)
{
this._members = members;
}
public void SetPropertyAndValue(string property, object value)
{
if (!_members.ContainsKey(property))
_members.Add(property, value);
else
_members[property] = value;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (!_members.ContainsKey(binder.Name))
_members.Add(binder.Name, value);
else
_members[binder.Name] = value;
return true;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (_members.ContainsKey(binder.Name))
{
result = _members[binder.Name];
return true;
}
else
{
return base.TryGetMember(binder, out result);
}
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
if (_members.ContainsKey(binder.Name)
&& _members[binder.Name] is Delegate)
{
result = (_members[binder.Name] as Delegate).DynamicInvoke(args);
return true;
}
else
{
return base.TryInvokeMember(binder, args, out result);
}
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return _members.Keys;
}
}
индексирование
Наши представления извлекут данные и вернутся как IDataReader. При индексации данных помощник по индексам будет перебирать программу чтения, а из программы чтения данные загружаются в Dynamic ElasticEntity, как показано ниже.
private ElasticEntity GetRecordAsElasticEntity(IDataReader reader)
{
ElasticEntity entity = new ElasticEntity();
for (int i = 0; i < reader.FieldCount; i++)
{
entity.SetPropertyAndValue(reader.GetName(i), reader.GetValue(i));
}
return entity;
}
Перед индексированием ElasticEntity он будет сериализован в json с использованием решения, предоставленного в Stackoverflow , он использует JavaScriptSerializer и очень быстрый. Такой же подход используется при десериализации результата json из Elasticsearch при поиске. Для десериализации результата Elasticsearch я изначально использовал json.net, но десериализация очень медленная по сравнению с сериализатором Javascript.
Ниже мой индексатор Elasticsearch. Это очень простой класс, который использует PlainElastic.Net.
public class PlainElasticIndexer:Interfaces.IElasticIndexer
{
private ElasticConnection _connection;
private string _indexName;
private string _type;
private string _defaultHost;
private int _port;
/// <summary>
/// Default host to localhost and port to 9200. If the host and port is different
/// then use the parameterized constructor and specify the details.
/// </summary>
private PlainElasticIndexer()
{
_connection = new ElasticConnection("localhost", 9200);
}
public PlainElasticIndexer(string host, int port, string indexName, string type)
{
this._defaultHost = host;
this._port = port;
_indexName = indexName;
_type = type;
_connection = new ElasticConnection(_defaultHost, _port);
}
/// <summary>
/// Default host to localhost and port to 9200. If the host and port is different
/// then use the parameterized constructor and specify the details.
/// </summary>
public PlainElasticIndexer(string indexName, string type):this()
{
_indexName = indexName;
_type = type;
}
/// <summary>
/// Add or update an index. If the ID exist then update the index with the provided json.
/// </summary>
/// <param name="json"></param>
/// <param name="id"></param>
public void Write(string json, string id)
{
string command = Commands.Index(_indexName, _type, id);
string response = _connection.Put(command, json);
}
}
Поиск
Поиск также использует очень простой класс, как показано ниже.
public class PlainElasticSearcher:Interfaces.IElasticSearcher
{
ElasticConnection _connection;
private string _indexName;
private string _type;
private string _defaultHost;
private int _port;
/// <summary>
/// Default host to localhost and port to 9200. If the host and port is different
/// then use the parameterized constructor and specify the details.
/// </summary>
private PlainElasticSearcher()
{
_connection = new ElasticConnection("localhost", 9200);
}
public PlainElasticSearcher(string host, int port, string indexName, string type)
{
this._defaultHost = host;
this._port = port;
_indexName = indexName;
_type = type;
_connection = new ElasticConnection(_defaultHost, _port);
}
/// <summary>
/// Default host to localhost and port to 9200. If the host and port is different
/// then use the other parameterized constructor and specify the details.
/// </summary>
public PlainElasticSearcher(string indexName, string type) : this()
{
_indexName = indexName;
_type = type;
}
public string Search(string jsonQuery)
{
string command = new SearchCommand(_indexName, _type).WithParameter("search_type", "query_and_fetch")
.WithParameter("size","100");
string result = _connection.Post(command, jsonQuery);
return result;
}
}
Функция поиска возвращает результат в виде простого json. Вызывающая сторона преобразует json в ElasticEntity, используя функцию, показанную ниже.
public static IList<ElasticEntity> ToElasticEntity(string json)
{
IList<ElasticEntity> results = new List<ElasticEntity>();
JavaScriptSerializer jss = new JavaScriptSerializer();
jss.RegisterConverters(new JavaScriptConverter[] { new DynamicJsonConverter() });
JsonTextReader jsonReader = new JsonTextReader(new StringReader(json));
var jObj = JObject.Parse(json);
foreach (var child in jObj["hits"]["hits"])
{
var tmp = child["_source"].ToString();
dynamic dynamicDict = jss.Deserialize(tmp, typeof(object)) as dynamic;
ElasticEntity elasticEntity = ElasticEntity.CreateFrom(dynamicDict);
results.Add(elasticEntity);
}
return results;
}
Я добавил другой класс для преобразования ElasticEntity в типизированный объект. Это помогает вызывающей стороне преобразовать ElasticEntity в объекты Domain или в DTO.
/// <summary>
/// This class maps ElasticEntity to any DomainSpecific strongly typed class. Say for e.g.
/// the requesting class wants the search result as a Customer class. This mapper will
/// create an instance of Customer and get the value from ElasticEntity and set
/// it to the Properties of Customer. One rule you should follow is
/// the Columns in the index and Properties in the Domain class should match.
/// We can say it's a convention based mapping.
/// </summary>
public class EntityMapper
{
private string _pathToClientEntityAssembly;
private Assembly _loadedAssembly = null;
public EntityMapper(string pathToClientEntityAssembly)
{
_pathToClientEntityAssembly = pathToClientEntityAssembly;
}
internal T Map<T>(ElasticEntity entity) where T:class
{
T instance = this.GetInstance<T>();
Type type = instance.GetType();
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo property in properties)
{
object value = entity.GetValue(property.Name);
property.SetValue(instance, Convert.ChangeType(value, property.PropertyType), null);
}
return instance;
}
private T GetInstance<T>()
{
if(string.IsNullOrWhiteSpace(_pathToClientEntityAssembly))
throw new NullReferenceException(string.Format("Unable to create {0}, path to the
assembley is not specified.", typeof(T).ToString()));
if(_loadedAssembly==null)
_loadedAssembly = Assembly.LoadFile(_pathToClientEntityAssembly);
object instance = _loadedAssembly.CreateInstance(typeof(T).ToString());
return (T)instance;
}
}
Клиент может вызвать функцию поиска, как показано ниже.
var searcher = new SearchFacade(new PlainElasticSearcher(), assmPath);
IList<Customer> resultAsCustomer = searcher.Search("Customer", jsonQry).Results<Customer>();
где assmPath — это путь к сборке, в которой находится объект Customer. Если пользователю нужен результат как ElasticEntity, установите для assmPath значение null и выполните вызов, как показано ниже.
IList<ElasticEntity> searchResult = searcher.Search("Customer", jsonQry).Results();
Ниже код показывает реализацию SearchFacade.
public class SearcherFacade
{
private IElasticSearcher _elasticSearcher;
private string _pathToEntityAssembly;
private IList<ElasticEntity> _searchResults;
/// <summary>
/// Use this constructor if the client will be dealing with dynamic ElasticEntity.
/// </summary>
/// <param name="elasticSearcher"></param>
public SearcherFacade(IElasticSearcher elasticSearcher)
{
_elasticSearcher = elasticSearcher;
}
/// <summary>
/// Use this constructor if the client wants to convert the ElasticEntity to domain specific class.
/// </summary>
/// <param name="elasticSearcher"></param>
/// <param name="pathToEntityAssembly">path to Assembley to locate the Domain specific class</param>
public SearcherFacade(IElasticSearcher elasticSearcher, string pathToEntityAssembly):this(elasticSearcher)
{
_pathToEntityAssembly = pathToEntityAssembly;
}
public SearcherFacade Search(string jsonQuery)
{
string jsonResult = _elasticSearcher.Search(jsonQuery);
_searchResults= DeserializeJson.ToElasticEntity(jsonResult);
return this;
}
public IList<ElasticEntity> Results()
{
return _searchResults;
}
/// <summary>
/// Converts dynamic ElasticEntity to strongly typed Domain Entity.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public IList<T> Results<T>() where T:class
{
if (string.IsNullOrWhiteSpace(_pathToEntityAssembly))
throw new NullReferenceException(string.Format("Please provide the Path and Assembly
name in which class {0} resides. Set the fully qualified Assembly path
via the constructor that take two parameters.", typeof(T).ToString()));
EntityMapper mapper = new EntityMapper(_pathToEntityAssembly);
IList<T> convertedResults = new List<T>();
foreach (ElasticEntity entity in _searchResults)
{
T instance = mapper.Map<T>(entity);
convertedResults.Add(instance);
}
return convertedResults;
}
}
Этот пост дает очень упрощенное и базовое представление о нашем слое ElasticSearch, который я создал. У нас есть больше функциональности с учетом наших потребностей. Надеюсь, этот пост поможет кому-то построить систему с помощью ElasticSearch.
Счастливое Кодирование