Статьи

Индексирование и поиск с ElasticSearch

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

Счастливое Кодирование