Последние пару дней я экспериментировал с 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.
Счастливое Кодирование