BI Consult Desktop Logo BI Consult Mobile Logo
  • Russian BI Исследование российских bi
  • Перейти на Fine BI
  • Контакты
  • +7 812 334-08-01
    +7 499 608-13-06
  • Отправить сообщение
  • Главная
  • Продукты Эксперт-BI
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Сельское хозяйство
    • Энергетика
    • FMCG
    • Девелоперы
    • Маркетплейсы
    • Пищевая промышленность
    • Фармацевтика
    • Построение Data Platform
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и FP&A
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • IBP
    • ИТ (CIO)
    • Закупки
  • Платформы
    • Системы бизнес-анализа (BI)
    • Интегрированное бизнес-планирование (IBP)
    • Хранилища данных (DWH / Lakehouse)
    • Каталоги данных (Data Catalog)
    • Системы ETL и ELT
    • AI / Исскуственный интеллект
    • Шина данных (ESB)
    • Система управления мастер-данными (MDM)
    • Семантический слой
  • Услуги
    • Переход на отечественные BI и DWH системы
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений и DWH
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Курсы
    • Учебный курс Информационная грамотность (Data Literacy)
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Greenplum
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt (Data Build Tool)
  • Компания
    • Руководство
    • Новости
    • Клиенты
    • Карьера
    • Скачать
    • Контакты

BI

  • FineBI
  • FineReport
  • FineDataLink
  • FineChatBI (FineAI)
  • Коннекторы данных из 1С в BI
  • Airflow / Nifi
  • Visiology
  • PIX BI
  • Modus BI
  • Yandex.DataLens
  • Open-source BI: Superset/Metabase
  • Luxms BI
  • AW BI + Alpha BI
  • FlyBI + Форсайт. Аналитическая Платформа
  • Loginom
  • Триафлай
  • AI / Исскуственный интеллект
  • Optimacros
  • Навигатор BI
  • Семантический слой

СУБД

  • Arenadata
  • ClickHouse
  • Greenplum
  • Postgres Professional
  • TData

Другое

  • Построение Data Platform
    • Аналитическое хранилище данных
    • Data Lake и Data Engineering
    • Подробнее про Data Lake
    • Внедрение Lakehouse
      • Apache Doris
      • StarRocks
      • Trino
    • Миграция витрин из пропиетарных DWH на новый стек
    • Учебный курс "Современная архитектура хранилища данных"
Главная » Курсы по системам бизнес-анализа и методологии » Учебный курс Современная архитектура хранилища данных » Как интегрировать Elasticsearch с DRF

Как интегрировать Elasticsearch с DRF

Из этой статьи Вы узнаете, как создать простой поисковый API с помощью Elasticsearch и Django-Rest-Framework (DRF).

Elasticsearch - это ведущая поисковая система в мире.

Многие крупные компании, например, Uber и Slack, используют именно ее,  но использовать эту технологию в Django довольно сложно.

Есть три разные библиотеки, которые Вам придется использовать, если Вы не хотите реализовать этот  проект с нуля.

Документация также довольно сложна для понимания, и на эту тему написано не так много.

Вот почему после нескольких дней упорной борьбы я все-таки решил написать эту статью, которая станет универсальным руководством по интеграции ElasticSearch с Django.

PS. Вы можете найти подобный проект на GitHub ниже.

 

Необходимые условия

  • Знание основ Python
  • Знание основ  Django и Django-Rest-Framework (DRF)
  • Знание основ ElasticSearch
  • Docker или локальная установка Elasticsearch

 

В чем состоит суть проекта?

Чтобы не терять времени, у меня уже есть готовое приложение Django с двумя моделями.

  • Статьи
  • Категории

Код для моделей:

from django.db import models
class Category(models.Model):
    title = models.CharField(max_length=100)class Article(models.Model):
    title = models.CharField(max_length=100)
    category = models.ForeignKey(
               Category, related_name='category', on_delete=models.CASCADE
               )

 

Основная цель данной статьи состоит в следующем:

  • Предоставить пользователям возможность искать статьи по названию с помощью ElasticSearch.
  • Предоставить пользователям возможность фильтровать статьи по категориям с помощью ElasticSearch.
  • Создать API для автозаполнения статей.

 

Приступим.

 

 

Локальный запуск Elasticsearch

Прежде чем приступить к работе с Django, нам нужно запустить экземпляр Elasticsearch.

Если у Вас уже есть локальный экземпляр, Вы можете смело пропустить эту часть.

Я предпочитаю использовать Docker и Docker-Compose.

Вот Dockerfile, который я использую для Django:

# Dockerfile
# syntax=docker/dockerfile:1
FROM python:3
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/

 

Вот мой файл docker-compose.yml file с двумя сервисами:

  • Web
  • Elasticsearch

 

Код:

version: "3.9"services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    depends_on:
      - elasticsearch
    networks:
      - elastic
  elasticsearch:
    image: elasticsearch:7.14.0
    volumes:
      - ./data/elastic:/var/lib/elasticsearch/data
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
    networks:
      - elastic
networks:
  elastic:
    driver: bridge

 

После того как эти два файла будут размещены в корневом каталоге, можно запускать контейнеры.

docker-compose up -d

 

Теперь идите сюда: localhost:8000. Все должно работать, как часы.

 

Интеграция Elasticsearch и Django

Интеграция с Elasticsearch довольно проста, Вам нужна всего одна библиотека.

Ее название - django-elasticsearch-dsl, устанавливаем.

pip install django-elasticsearch-dsl

 

Имейте в виду, что для разных версий ElasticSearch существуют разные версии библиотеки.

По состоянию на ноябрь 2021 года требования следующие:

  • Для Elasticsearch 7.0 и более поздних версий используйте основную версию библиотеки 7 (7.x.y).
  • Для Elasticsearch 6.0 и более поздних версий используйте основную версию библиотеки 6 (6.x.y).
  • Для Elasticsearch 5.0 и более поздних версий используйте основную версию библиотеки 0.5 (0.5.x).

 

После установки библиотеки не забудьте добавить ее в список installed_apps в файле settings.py

# settings.pyINSTALLED_APPS = [
    # Other apps
    'django_elasticsearch_dsl',
]

 

Наконец, нам просто нужно указать django-elastsearch-dsl, где находится наш экземпляр ElasticSearch.

Добавьте это в файл settings.py:

# settings.py# other code....ELASTICSEARCH_DSL = {
    'default': {
        'hosts': 'elasticsearch:9200'
    },
}

 

Здесь мы говорим, что ElasticSearch находится по адресу elasticsearch:9200, что эквивалентно localhost:9200, но мы используем имя сервиса при обмене данными между двумя сервисами docker-compose.

На этом этапе все должно работать как обычно.

 

Интеграция Elasticsearch с DRF

Теперь интегрируем Elasticsearch с DRF.

Для этого нам понадобится еще одна библиотека под названием django-elasticsearch-dsl-drf

pip install django-elasticsearch-dsl-drf

 

Мы должны добавить его в installed_apps в файле settings.py:

# settings.pyINSTALLED_APPS = [
    # Other apps
    'django_elasticsearch_dsl',
    'django_elasticsearch_dsl_drf',
]

 

Вот и все, мы полностью интегрировали Elasticsearch с DRF.

Теперь давайте создадим несколько API.

 

Индексируем нашу модель в Elasticsearch

ElasticSearch работает, но он пуст.

Нам нужно сказать Django, чтобы он скопировал наши существующие записи в ElasticSearch.

ElasticSearch хранит данные в документах JSON.

Это означает, что для каждой модели, которую мы хотим добавить в ElasticSearch, мы должны создать класс документа.

Он будет храниться в файле documents.py в Вашей папке apps.

Давайте создадим документ для нашей модели статьи:

# articles/documents.pyfrom django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registryfrom articles.models import Article
@registry.register_document
class ArticleDocument(Document):
    title = fields.TextField(
        attr='title',
        fields={
            'raw': fields.TextField(),
            'suggest': fields.CompletionField(),
        }
    )
    category = fields.ObjectField(
        attr='category',
        properties={
            'id': fields.IntegerField(),
            'title': fields.TextField(
                attr='title',
                fields={
                    'raw': fields.KeywordField(),
                }
            )
        }
    )    class Index:
        name = 'articles'    class Django:
        model = Article

 

Это может показаться сложным, но мы просто зарегистрировали документ ArticleDocument, связанный с моделью Article. Затем мы указали поля, которые хотим индексировать в ElasticSearch.

Первое поле - это заголовок, который представляет собой текстовое поле с двумя свойствами.

  • raw - обычное текстовое поле ElasticSearch, которое мы будем использовать для поиска.
  • suggest — поле, которое используется для функции автозаполнения.

 

Следующее поле - категория, которое является полем отношения, но ElasticSearch не имеет концепции отношений.

Поэтому, чтобы сохранить весь объект категории в Elasticsearch, мы используем поле object,.

Мы указываем, что поле category - это объект, затем в свойствах указываем поля category - id и title.

Вы могли заметить, что в заголовке категории мы используем поле ключевого слова вместо текстового поля. Разница между ними заключается в том, как они анализируются в ElasticSearch.

Наконец, мы указываем имя индекса для статей.

Но прежде чем двигаться дальше, мы должны зарегистрировать наши индексы.

Поэтому добавьте этот код в файл settings.py

# settings.py ELASTICSEARCH_INDEX_NAMES = { 'articles.article': 'articles', }

 

Используемый мной формат в app.model: index

Но прежде чем индексировать  модель, убедитесь в том, что в Вашей базе данных есть записи.

Как только Вы это сделаете, выполните команду:

python manage.py search_index --rebuild

 

Это позволит заполнить наш индекс статей данными из нашей базы данных.

Кажется, все работает, или нет?

Вы, наверное, заметили проблему: что если мы будем добавлять или удалять записи?

Будут ли индексы автоматически обновляться?

К сожалению, нет, но мы можем легко автоматизировать это с помощью Django Signals.

Сигналы предлагают хуки - код, который запускается после определенного события.

Нас интересуют хуки post_save и post_delete.

Поэтому создайте в своем приложении файл signals.py и добавьте в него этот код:

# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiverfrom django_elasticsearch_dsl.registries import registry
@receiver(post_save)
def update_document(sender, **kwargs):
    app_label = sender._meta.app_label
    model_name = sender._meta.model_name
    instance = kwargs['instance']    if app_label == 'articles':
        if model_name == 'article':
            instances = instance.article.all()
            for _instance in instances:
                registry.update(_instance)
@receiver(post_delete)
def delete_document(sender, **kwargs):
    app_label = sender._meta.app_label
    model_name = sender._meta.model_name
    instance = kwargs['instance']    if app_label == 'articles':
        if model_name == 'article':
            instances = instance.article.all()
            for _instance in instances:
                registry.update(_instance)

 

Этот код по сути обновляет индексы после создания или удаления статей.

Он проверяет имя приложения и имя модели для того, чтобы убедиться, что он запускается только для модели статьи.

 

Сериализация документа

Теперь, когда мы проиндексировали модель, нам нужно найти способ ее сериализации.

Здесь на помощь нам приходит django-elasticsearch-dsl-drf.

Библиотека предоставляет нам класс DocumentSerializer, который позволяет легко сериализовать документы.

Создайте файл serializers.py в своем приложении и добавьте в него:

from django_elasticsearch_dsl_drf.serializers import DocumentSerializerfrom articles.documents import ArticleDocumentclass ArticleDocumentSerializer(DocumentSerializer):
    class Meta:
        document = ArticleDocument        fields = (
            'title',
            'category'
        )

 

Он очень похож на ваш обычный DRF-сериализатор, вы просто передаете ему документ и указываете поля, которые Вы хотите сериализовать.

 

Создание API

Наконец, у нас все готово для создания наших API.

Мы будем шаг за шагом реализовывать функцию за функцией.

Давайте создадим базовое представление документа, без каких-либо функций.

В файле views.py добавьте:

from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
    document = ArticleDocument
    serializer_class = ArticleDocumentSerializer    filter_backends = []

 

Это простое представление документа, которое только перечисляет и извлекает статьи из нашего индекса ElascticSearch.

Опять же, это делается очень просто благодаря django-elasticsearch-dsl-drf

Давайте реализуем разные функции.

 

Поиск

Для реализации поиска нам нужно:

  1. добавить SearchFilterBackend к filter_backends
  2. добавить search_fields который принимает кортеж полей, по которым Вы хотите выполнить поиск.

 

from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
    document = ArticleDocument
    serializer_class = ArticleDocumentSerializer    filter_backends = [
               SearchFilterBackend
    ]
   
    search_fields = ('title',)

 

Фильтрация

Для реализации простой фильтрации нужно сделать следующее:

  1. добавить FilteringFilterBackend к filter_backends
  2. добавить filter_fields который принимает словарь полей, по которым Вы хотите отфильтровать информацию.

 

from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from django_elasticsearch_dsl_drf.filter_backends import FilteringFilterBackend
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
    document = ArticleDocument
    serializer_class = ArticleDocumentSerializer    filter_backends = [
               FilteringFilterBackend
    ]
   
    filter_fields = {
        'category': 'category.id'
    }

 

Вас может несколько удивить следующая линия:

'category': 'category.id'

 

По сути, это означает, что мне нужен параметр category, который принимает id, и фильтрует объект category именно по полю id.

 

Автозаполнение

Для реализации простого автозаполнения нужно:

  1. добавить SuggesterFilterBackend к filter_backends
  2. добавить suggester_fields который принимает словарь полей.

 

from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from articles.documents import ArticleDocument
from django_elasticsearch_dsl_drf.filter_backends import SuggesterFilterBackend
from django_elasticsearch_dsl_drf.constants import SUGGESTER_COMPLETION
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
    document = ArticleDocument
    serializer_class = ArticleDocumentSerializer    filter_backends = [
               SuggesterFilterBackend
    ]
   
    suggester_fields = {
        'title': {
            'field': 'title.suggest',
            'suggesters': [
                SUGGESTER_COMPLETION,
            ],
        },
    }

 

Разобьем этот код на части:

suggester_fields = {
        'title': {
            'field': 'title.suggest',
            'suggesters': [
                SUGGESTER_COMPLETION,
            ],
        },
    }

 

Первое правило заключается в том, что мы ДОЛЖНЫ автозаполнять по полю завершения.

Если Вы помните, в файле документа мы объявили свойство «suggest» для заголовка поля, которое должно быть полем завершения.

Далее мы указываем, какой тип предложения мы используем, по состоянию на октябрь 2021 года Elasticsearch поддерживает три типа предложений:

  • заполнение
  • фраза
  • термин

 

Нам достаточно заполнения.

 

Финальная версия представления

from django_elasticsearch_dsl_drf.constants import SUGGESTER_COMPLETION
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, FilteringFilterBackend, SuggesterFilterBackend
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSetfrom articles.documents import ArticleDocument
from articles.serializers import ArticleDocumentSerializerclass ArticleDocumentView(DocumentViewSet):
    document = ArticleDocument
    serializer_class = ArticleDocumentSerializer    filter_backends = [
        FilteringFilterBackend,
        SearchFilterBackend,
        SuggesterFilterBackend
    ]    search_fields = (
        'title',
    )    filter_fields = {
        'category': 'category.id'
    }    suggester_fields = {
        'title': {
            'field': 'title.suggest',
            'suggesters': [
                SUGGESTER_COMPLETION,
            ],
        },

 

Обязательно добавьте все это в urls.py

# urls.py
from django.contrib import admin
from django.urls import path
from rest_framework import routersfrom articles.views importArticleDocumentViewrouter = routers.SimpleRouter(trailing_slash=False)router.register(r'article-search', ArticleDocumentView, basename='article-search')urlpatterns = [
    path('admin/', admin.site.urls),
]urlpatterns += router.urls

 

API в действии

 

DocumentViewSet автоматически создает три различных API.

  • Один для создания списка и извлечения записей.
  • Один для обычной функциональности (поиск, фильтр и т. д.).
  • Один для функциональности автозаполнения.
  • Один для функциональных предложений.

 

В нашем примере нам важна только обычная функциональность и функциональность предложений.

 

Перечень

URL: localhost:8000/article-search

 

Поиск

URL: localhost:8000/article-search?search=programming

Если Вы хотите выполнить поиск по нескольким терминам, Вы можете сделать это с помощью:

localhost:8000/article-search?search=term1&search=term2

 

Подробнее об этом Вы можете прочитать здесь:

Документация django-elasticsearch-dsl-drf 0.22.2

 

Фильтрация

URL: localhost:8000/article-search?category=2

Существует множество различных способов фильтрации.

Например, если Вы хотите отфильтровать статьи по нескольким категориям, Вы можете сделать следующее:

localhost:8000/article-search?category=id1__id2__id3

 

Подробнее о различных фильтрах Вы можете прочитать здесь.

Документация django-elasticsearch-dsl-drf 0.22.2

 

Автозаполнение

URL: localhost:8000/article-search/suggest?title__completion=how

 

Заключение

Прежде всего, поздравляем Вас с тем, что Вы дошли до этого места!

В завершение давайте подытожим, что же мы сегодня узнали:

  • Мы создали локальный экземпляр ElasticSearch с помощью Docker и Docker-Compose.
  • Мы связали Django с ElasticSearch с помощью библиотеки django-elasticsearch-dsl.
  • Мы проиндексировали наши модели с помощью документов.
  • Мы синхронизировали наши модели и индексы с помощью сигналов.
  • Мы сериализовали наши документы.
  • Мы создали базовые функции поиска, фильтрации и автозаполнения.

 

Узнать стоимость решенияЗапросить видео презентацию

← Предыдущая статья
Как создать приложение для поиска с автозаполнением с помощью Elasticsearch и NestJS
Следующая статья →
RUST и инжиниринг данных - часть 1
Запросить видео презентацию Запросить доступ к демо стенду online Узнать стоимость лицензий

Задать вопрос

loading...

Решения

Анализировать ФинансыУвеличивайте ПродажиОптимальный Склад и ЛогистикаМаркетинговые Метрики

Клиенты
  • Группа компаний «Галакс» ведет свою деятельность с 2005 года, являясь в те годы дистрибьютором известных международных марок в ряде крупнейших торговых сетей России в сегменте аудио и видео аксессуаров. Активно работая в этом направлении и приобретая ценный опыт, начали создавать собственные торговые марки «GAL» и «VIXTER»

  • ООО «Модум-Транс» — независимый оператор грузовых железнодорожных перевозок, лидирующий по количеству инновационного парка на сети РЖД.

  • "Уральский банк реконструкции и развития" входит в топ-25 крупнейших банков России и список значимых кредитных организаций на рынке платежных услуг по версии ЦБ РФ.

  • В 2003 году Мерсико и пятью микрокредитными агентствами Мерсико было принято историческое решение о консолидации активов по всей территории Кыргызстана в целях образования национального финансового института по развитию сообществ - Компаньона. В октябре 2004 года Компаньон был зарегистрирован Национальным банком Кыргызской Республики.

  • Решения
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Энергетика
    • Фармацевтика
  • Услуги
    • Переход на отечественные BI и DWH
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Техническая поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Платформы
    • FineBI
    • FineReport
    • FineDataLink
    • Коннекторы данных из 1С в BI
    • Airflow + NiFi
    • Visiology
    • Luxms BI
    • Modus BI
    • PIX BI
    • Arenadata
    • ClickHouse
    • Greenplum
    • Postgres Professional
    • Open-source BI: Superset/Metabase
    • Loginom
    • Yandex.DataLens
    • AI / Исскуственный интеллект
    • Optimacros
    • Шины данных
  • Курсы
    • Учебный курс Информационная грамотность
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt
  • Функциональные решения
    • Создание Data Lake
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и прогнозная аналитика
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • Сквозная аналитика
  • Компания
    • О нас
    • Руководство
    • Новости
    • Клиенты
    • Скачать
    • Контакты
    • Политика конфиденциальности
RutubeVkontakteLinkedInYouTube
ООО "Би Ай Консалт",
ИНН: 7811437757,
ОГРН: 1097847154184
199178, Россия,
Санкт-Петербург,
6-ая линия В.О., Д. 63, 4 этаж
Тел: +7 (812) 334-08-01
Тел: +7 (499) 608-13-06
E-mail: info@biconsult.ru

 

 

 

 

 

×

Пользуясь сайтом, вы соглашаетесь с использованием cookies и политикой конфиденциальности.