- PVSM.RU - https://www.pvsm.ru -
[1]
Спросите разработчика: «Как бы вы реализовали функцию поиска в своем продукте? [2]» или «Как создать поисковую систему? [3]». Вероятно, в ответ вы услышите что-нибудь такое: «Ну, мы просто запустим кластер Elasticsearch: с поиском сегодня всё просто».
Но так ли это? Во многих современных продуктах по-прежнему [4] не лучшим [5] образом [6] реализован [7] поиск [8]. Настоящий специалист по поисковым системам скажет вам, что лишь немногие разработчики глубоко понимают, как работает поиск, а ведь это знание часто необходимо для улучшения качества поиска.
Есть множество программных пакетов с открытым исходным кодом, проведено немало исследований, однако лишь немногие избранные понимают, как нужно делать функциональный поиск. Как ни забавно, но если поискать в Интернете [9] связанную с реализацией поиска информацию, вы не найдете актуальных и содержательных обзоров.
Этот текст можно считать собранием ценных идей и ресурсов, которые могут помочь в создании функции поиска. Статья, безусловно, не претендует на исчерпывающую полноту, однако я надеюсь, что ваши отзывы помогут ее доработать (оставляйте замечания в комментариях или свяжитесь со мной).
Основываясь на опыте работы с универсальными решениями и узкоспециализированными проектами самого разного масштаба (в компаниях Google, Airbnb и нескольких стартапах), я расскажу о некоторых популярных подходах, алгоритмах, методах и инструментах.
Недооценка и непонимание масштабов и сложности задачи поиска могут привести к тому, что у пользователей останутся плохие впечатления, разработчики потратят время впустую, а продукт провалится.
Переведено в Alconost [10]
Если вам не терпится перейти к практике или вы многое по этой теме уже знаете, возможно, стоит сразу перескочить в раздел инструментов и сервисов.
Статья длинная. Однако большая часть материала в ней опирается на четыре основных принципа:
У каждого продукта — свой поиск. Выбор решения зависит от множества технических характеристик и требований. Полезно определить ключевые параметры конкретной задачи поиска:
Если продумать перечисленные вопросы заранее, это поможет сделать важный выбор в проектировании и создании отдельных компонентов поисковой системы.
Конвейер индексации в работе.
Пришло время пройтись по списку подзадач в построении поисковой системы, которые обычно решаются отдельными подсистемами, образующими конвейер: иными словами, каждая подсистема получает выходные данные предыдущих подсистем и выдает входные данные для последующих подсистем.
Это приводит нас к важному свойству всей экосистемы: изменив работу какой-либо подсистемы, нужно оценить, как это повлияет на следующие за ней подсистемы, и, возможно, изменить их поведение тоже.
Рассмотрим самые важные практические задачи, которые придется решать.
Берем набор документов (например, весь Интернет, все сообщения сети Twitter или фото на сервисе Instagram), выбираем потенциально меньшее подмножество документов, которые есть смысл рассматривать как результаты поиска и включаем в индекс только их, отбрасывая остальные. Это почти не зависит от выбора документов для показа пользователю и нужно для того, чтобы индекс был компактным. Не подойти для индекса могут, например, следующие классы документов.
Поисковый спам самых разных форм и размеров — это объемная тема, которая сама по себе достойна отдельного руководства. Здесь — хороший обзор таксономии интернет-спама [13].
При некоторых ограничениях области поиска может потребоваться фильтрация: придется отбросить порнографию [14], незаконные материалы и т. д. Соответствующие методы похожи на фильтрацию спама, но могут включать в себя и специальные эвристические алгоритмы.
В том числе почти копии и избыточные документы. Здесь могут помочь хеширование с чувствительностью к местоположению [15], мера сходства [16], методы кластеризации и даже данные кликов [17]. Здесь — хороший обзор [18] таких методов.
Определение полезности зависит от области работы поиска, поэтому порекомендовать конкретные подходы трудно. Пригодиться могут следующие соображения. Вероятно, для документов можно будет построить функцию полезности. Можно попробовать эвристику; или, например, изображение, содержащее только черные пиксели — как образец бесполезного документа. Полезность можно оценить, опираясь на поведение пользователя.
В большинстве поисковых систем выборка документов выполняется посредством обращенного индекса [19], который часто называется просто индексом.
Самые популярные поисковые системы принимают неструктурированные запросы. Это означает, что система должна извлечь структуру из самого запроса. В случае обращенного индекса извлекать поисковые термины нужно с помощью методов NLP [27].
Извлеченные термины могут использоваться для выборки соответствующих документов. К сожалению, в большинстве случаев запросы сформулированы не очень хорошо, поэтому необходимо дополнительно расширять и переписывать их, например, следующим образом:
Дается список документов (полученных на предыдущем шаге), их сигналы и обработанный запрос и формируется оптимальный порядок этих документов (что и называется ранжированием).
Первоначально большинство используемых моделей ранжирования представляли собой подстроенные вручную взвешенные сочетания всех сигналов документов. Наборы сигналов могут включать в себя PageRank, данные кликов, сведения об актуальности и другое [35].
Чтобы жизнь медом не казалась, многие подобные сигналы, например, PageRank и сформированные статистическими языковыми моделями [36] сигналы, содержат параметры, которые значительно влияют на работу сигнала. И они тоже требуют ручной подстройки.
В последнее время все более популярным становится обучение ранжированию [37] — основанные на сигналах дифференциальные подходы с учителем. Среди популярных LtR в качестве примера можно привести McRank [38] и LambdaRank [39] от Microsoft, а также Матрикснет [40] от «Яндекса».
Также в области семантического поиска и ранжирования сейчас набирает популярность новый подход на основе векторных пространств [41]. Задумка в том, чтобы обучить отдельные низкоразмерные векторные представления документа, а затем построить модель, которая будет отображать запросы в это векторное пространство.
В таком случае при выборке нужно просто найти несколько документов, которые по некоторому показателю находятся ближе всего к вектору запроса (например, по евклидову расстоянию). Это расстояние и будет рангом. Если хорошо построить отображение и документов, и запросов, то документы будут выбираться не по наличию какого-либо простого шаблона (например, слова), а по тому, насколько близки документы к запросу по смыслу.
Обычно, чтобы сохранять актуальность поискового индекса и функции поиска, все рассмотренные части конвейера должны быть под постоянным контролем.
Управление поисковым конвейером может оказаться сложной задачей, поскольку вся система состоит из множества подвижных частей. Ведь конвейер — это не только перемещение данных: с течением времени меняются также код модулей, форматы и допущения, включенные в данные.
Конвейер можно запускать в «пакетном» режиме, на регулярной, нерегулярной основе (если не нужно индексировать в реальном времени), в потоковом режиме (если без индексирования в реальном времени не обойтись) или по определенным триггерам.
Некоторые сложные поисковые системы (например, Google) используют конвейеры в несколько уровней — на разных временных масштабах: например, часто изменяющаяся страница (тот же cnn.com [42]) индексируется чаще, чем статическая страница, которая не менялась годами.
Конечная цель поисковой системы — принимать запросы и посредством индекса возвращать соответствующим образом ранжированные результаты. Вопрос обслуживающих систем может быть очень сложным и включать в себя множество технических подробностей, но я все-таки упомяну несколько ключевых аспектов этой части поисковых систем.
Оценка человеком. Да, такая работа в поисковых системах все еще нужна.
Итак, вы запустили собственный конвейер индексации и поисковые серверы; все работает хорошо. К сожалению, запуск инфраструктуры — лишь начало пути к хорошему поиску.
Далее нужно будет создать набор процессов непрерывной оценки и повышения качества поиска. Это на самом деле и есть основная часть работы — и самая сложная задача, которую придется решать.
Что такое качество? Во-первых, нужно определить (и заставить своего начальника или руководителя проекта согласиться), что значит «качество» в конкретном случае:
Поговорим о показателях. Некоторые из следующих понятий бывает сложно оценить количественно. И в то же время было бы невероятно полезно одним числом — показателем качества — выразить, насколько хорошо работает поисковая система.
Непрерывный расчет такого показателя для своей системы (а также для конкурентов) позволяет отследить прогресс и показать начальнику, насколько хорошо вы делаете свою работу. Вот несколько классических методов количественной оценки качества, которые помогут построить собственную волшебную формулу оценки качества:
Оценка человеком. Может возникнуть ощущение, что показатели качества — это результат статистических расчетов, однако их нельзя получить автоматически. В конечном итоге показатели должны отражать субъективную человеческую оценку, и именно здесь вступает в игру человеческий фактор.
Построение поисковой системы без оценки результатов человеком — это, вероятно, самая распространенная причина плохой работы поиска.
Обычно на первых порах разработчики сами вручную оценивают результаты. Чуть позже привлекаются оценщики [55], которые для просмотра возвращаемых результатов поиска и отправки своей оценки качества обычно используют специальные инструменты.
Разработчик может использовать полученные таким образом сигналы, чтобы скорректировать курс разработки, принять решение о запуске и даже передать эти данные на этап выбора индекса и в системы выборки и ранжирования.
Вот несколько иных видов «человеческой» оценки, которые могут выполняться в поисковой системе:
Об оценочных наборах данных, таких как упомянутые выше тестовые выборки, следует начинать думать на ранних этапах проектирования поиска. Как их собирать и обновлять? Как внедрять в рабочий конвейер оценки? Насколько они репрезентативны?
Эксперименты в реальном времени. После того как поисковая система заманит достаточно пользователей, на части трафика можно начинать экспериментировать в реальном времени [56]. Суть в том, чтобы для группы людей включить некоторую оптимизацию, а затем сравнить результат с «контрольной» группой — аналогичной выборкой пользователей, у которых ничего не изменялось. Оцениваемый в таком исследовании показатель зависит от продукта: это могут быть клики по результатам поиска, клики по рекламе и т. д.
Периодичность оценки. Скорость совершенствования поиска напрямую связана с тем, насколько быстро можно проворачивать описанный выше цикл измерения и доработки. Нужно с самого начала задаться вопросом: «Как быстро мы можем измерять и повышать производительность?».
Сколько понадобится времени, чтобы внести изменения и дождаться результатов: дни, часы, минуты или секунды? ️ Кроме прочего, процедура запуска оценки должна быть максимально простой и не должна отнимать слишком много времени.
Эта статья не задумывалась как руководство. Тем не менее, я кратко опишу, как я сам подошел бы к разработке функции поиска сегодня:
2. Если размещение на сервере не отвечает требованиям проекта или на это нет средств, возможный выбор в этом случае — библиотека или инструмент с открытым кодом. На данный момент для «подключенных» приложений и веб-сайтов я бы выбрал Elasticsearch. Для встроенных систем некоторые инструменты перечислены ниже.
3. Перед загрузкой данных в поисковый индекс вы скорее всего захотите выбрать собственно индекс, а также подчистить документы (например, извлечь нужный текст из HTML-страниц), что уменьшит размер индекса и упростит получение хорошей выдачи. Если корпус влазит на одну машину, просто напишите для этого пару скриптов. Если нет — я бы использовал Spark [58].
Инструментов много не бывает.
Algolia [12] — проприетарный SaaS, который индексирует веб-сайт клиента и дает API для поиска по его страницам. У них есть API для отправки документов клиента, поддержка контекстно-зависимых запросов, да и работает их система очень быстро. Если внедрять сегодня веб-поиск, сначала я бы использовал Algolia (если это по карману) — так можно было бы выиграть время на создание собственного аналогичного поиска.
Lucene [64] — самая популярная библиотека информационного поиска. Умеет анализировать запросы, делать ранжирование и выборку по индексу. Любой из компонентов можно заменить на альтернативную реализацию. Есть также порт на C — Lucy [65].
Несколько интересных и полезных наборов данных, которые помогут построить поисковую систему или оценить ее качество:
Это была моя скромная попытка сделать хоть сколь-нибудь полезную «карту» для тех, кто начинает разрабатывать поисковую систему. Если я пропустил что-то важное — пишите.
О переводчике
Перевод статьи выполнен в Alconost.
Alconost занимается локализацией игр [105], приложений и сайтов [106] на 68 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.
Мы также делаем рекламные и обучающие видеоролики [107] — для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.
Подробнее: https://alconost.com [10]
Автор: alconost
Источник [108]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/265492
Ссылки в тексте:
[1] Image: https://habrahabr.ru/company/alconost/blog/339894/
[2] Как бы вы реализовали функцию поиска в своем продукте?: https://stackoverflow.com/questions/34314/how-do-i-implement-search-functionality-in-a-website
[3] Как создать поисковую систему?: https://www.quora.com/How-to-build-a-search-engine-from-scratch
[4] по-прежнему: https://github.com/isaacs/github/issues/908
[5] не лучшим: https://www.reddit.com/r/Windows10/comments/4jbxgo/can_we_talk_about_how_bad_windows_10_search_sucks/d365mce/
[6] образом: https://www.reddit.com/r/spotify/comments/2apwpd/the_search_function_sucks_let_me_explain/
[7] реализован: https://medium.com/@RohitPaulK/github-issues-suck-723a5b80a1a3#.yp8ui3g9i
[8] поиск: https://thenextweb.com/opinion/2016/01/11/netflix-search-sucks-flixed-fixes-it/
[9] поискать в Интернете: https://www.google.com/search?q=%D1%81%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5+%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%D0%BE%D0%B9+%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D1%8B
[10] Alconost: https://alconost.com?utm_source=habrahabr&utm_medium=article&utm_campaign=translation&utm_content=about-search
[11] планирование авиаперелетов — очень трудоемкая задача: http://www.demarcken.org/carl/papers/ITA-software-travel-complexity/ITA-software-travel-complexity.pdf
[12] Algolia: https://www.algolia.com/
[13] Здесь — хороший обзор таксономии интернет-спама: http://airweb.cse.lehigh.edu/2005/gyongyi.pdf
[14] порнографию: https://www.researchgate.net/profile/Gabriel_Sanchez-Perez/publication/262371199_Explicit_image_detection_using_YCbCr_space_color_model_as_skin_detection/links/549839cf0cf2519f5a1dd966.pdf
[15] хеширование с чувствительностью к местоположению: https://ru.wikipedia.org/wiki/Locality-sensitive_hashing
[16] мера сходства: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D1%8D%D1%84%D1%84%D0%B8%D1%86%D0%B8%D0%B5%D0%BD%D1%82_%D1%81%D1%85%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%B0
[17] данные кликов: https://www.microsoft.com/en-us/research/wp-content/uploads/2011/02/RadlinskiBennettYilmaz_WSDM2011.pdf
[18] Здесь — хороший обзор: http://infolab.stanford.edu/~ullman/mmds/ch3.pdf
[19] обращенного индекса: https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%B2%D0%B5%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81
[20] выделение основы слов: https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BC%D0%BC%D0%B8%D0%BD%D0%B3
[21] извлечение объектов: https://en.wikipedia.org/wiki/Named-entity_recognition
[22] PageRank: http://ilpubs.stanford.edu:8090/422/1/1999-66.pdf
[23] темы: https://gofishdigital.com/semantic-topic-modeling/
[24] методы сжатия предметных указателей: https://nlp.stanford.edu/IR-book/html/htmledition/postings-file-compression-1.html
[25] отображение данных через mmap(): https://deplinenoise.wordpress.com/2013/03/31/fast-mmapable-data-structures/
[26] LSM-дерево: https://ru.wikipedia.org/wiki/LSM-%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE
[27] NLP: https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0_%D0%B5%D1%81%D1%82%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE_%D1%8F%D0%B7%D1%8B%D0%BA%D0%B0
[28] Повторная весовая обработка терминов: http://orion.lcg.ufrj.br/Dr.Dobbs/books/book5/chap11.htm
[29] Проверка орфографии: http://norvig.com/spell-correct.html
[30] Поиск: http://nlp.stanford.edu/IR-book/html/htmledition/query-expansion-1.html
[31] синонимов: https://www.iro.umontreal.ca/~nie/IFT6255/carpineto-Survey-QE.pdf
[32] языковое моделирование на основе скрытой марковской модели (HMM): http://www.aclweb.org/anthology/P02-1060
[33] персонализации: https://en.wikipedia.org/wiki/Personalized_search
[34] локального контекста: http://searchengineland.com/future-search-engines-context-217550
[35] другое: http://backlinko.com/google-ranking-factors
[36] статистическими языковыми моделями: http://times.cs.uiuc.edu/czhai/pub/slmir-now.pdf
[37] обучение ранжированию: https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5_%D1%80%D0%B0%D0%BD%D0%B6%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8E
[38] McRank: https://papers.nips.cc/paper/3270-mcrank-learning-to-rank-using-multiple-classification-and-gradient-boosting.pdf
[39] LambdaRank: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/lambdarank.pdf
[40] Матрикснет: https://yandex.ru/company/technologies/matrixnet/
[41] подход на основе векторных пространств: https://arxiv.org/abs/1708.02702
[42] cnn.com: http://cnn.com/
[43] обширное исследование: http://services.google.com/fh/files/blogs/google_delayexp.pdf
[44] по этой теме: http://highscalability.com/latency-everywhere-and-it-costs-you-sales-how-crush-it
[45] кэш нужно подготовить — «разогреть»: https://stackoverflow.com/questions/22756092/what-does-it-mean-by-cold-cache-and-warm-cache-concept
[46] сложная задача: https://en.wikipedia.org/wiki/Cache_performance_measurement_and_metric
[47] Приверженность пользователей: http://blog.popcornmetrics.com/5-user-engagement-metrics-for-growth/
[48] точности: https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA#.D0.A2.D0.BE.D1.87.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.28precision.29
[49] полноты: https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA#.D0.9F.D0.BE.D0.BB.D0.BD.D0.BE.D1.82.D0.B0_.28recall.29
[50] F-мера: https://en.wikipedia.org/wiki/F1_score
[51] Средняя точность: http://fastml.com/what-you-wanted-to-know-about-mean-average-precision/
[52] Приведенная суммарная эффективность релевантности: https://en.wikipedia.org/wiki/Discounted_cumulative_gain
[53] «Длинные» и «короткие» клики: http://www.blindfiveyearold.com/short-clicks-versus-long-clicks
[54] Здесь — хороший подробный обзор показателей: https://arxiv.org/pdf/1302.2318.pdf
[55] оценщики: http://static.googleusercontent.com/media/www.google.com/en//insidesearch/howsearchworks/assets/searchqualityevaluatorguidelines.pdf
[56] экспериментировать в реальном времени: https://googleblog.blogspot.co.uk/2008/08/search-experiments-large-and-small.html
[57] запросов в секунду: https://en.wikipedia.org/wiki/Queries_per_second
[58] Spark: https://spark.apache.org/
[59] Elasticsearch Cloud: https://aws.amazon.com/elasticsearch-service/
[60] elastic.co: https://www.elastic.co/
[61] Qbox: https://qbox.io/
[62] Поиск Azure: https://azure.microsoft.com/ru-ru/services/search/
[63] Swiftype: https://swiftype.com/
[64] Lucene: https://lucene.apache.org/
[65] Lucy: https://lucy.apache.org/
[66] Solr: http://lucene.apache.org/solr/
[67] Hadoop: http://hadoop.apache.org/
[68] Spark: http://spark.apache.org/
[69] EMR: https://aws.amazon.com/emr/
[70] Elasticsearch: https://www.elastic.co/products/elasticsearch
[71] здесь можно сравнить Elasticsearch и Solr: http://solr-vs-elasticsearch.com/
[72] функциональный API: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html
[73] его можно интегрировать в Hadoop: https://github.com/elastic/elasticsearch-hadoop
[74] хорошо масштабируется: https://www.elastic.co/guide/en/elasticsearch/guide/current/distributed-cluster.html
[75] Enterprise: https://www.elastic.co/cloud/enterprise
[76] Xapian: https://xapian.org/
[77] Sphinx: http://sphinxsearch.com/
[78] подсистема хранилища для MySQL: https://mariadb.com/kb/en/mariadb/sphinx-storage-engine/
[79] Nutch: https://nutch.apache.org/
[80] Common Crawl: http://commoncrawl.org/
[81] Lunr: https://lunrjs.com/
[82] SearchKit: https://github.com/searchkit/searchkit
[83] Norch: https://github.com/fergiemcdowall/norch
[84] LevelDB: https://github.com/google/leveldb
[85] Whoosh: https://bitbucket.org/mchaput/whoosh/wiki/Home
[86] набор поискового ПО: http://wiki.openstreetmap.org/wiki/Search_engines
[87] AWS есть зеркало: https://aws.amazon.com/public-datasets/common-crawl/
[88] Дамп данных OpenStreetMap: http://wiki.openstreetmap.org/wiki/Downloading_data
[89] N-граммы Google Книг: http://commondatastorage.googleapis.com/books/syntactic-ngrams/index.html
[90] Дампы «Википедии»: https://dumps.wikimedia.org/
[91] множество соответствующих вспомогательных инструментов: https://www.mediawiki.org/wiki/Alternative_parsers
[92] Дампы IMDb: http://www.imdb.com/interfaces
[93] Modern Information Retrieval: https://www.amazon.com/dp/0321416910
[94] Information Retrieval: https://www.amazon.com/dp/0262528878/
[95] Learning to Rank: https://www.amazon.com/dp/3642142664/
[96] Managing Gigabytes: https://www.amazon.com/dp/1558605703
[97] Text Retrieval and Search Engines: https://www.coursera.org/learn/text-retrieval
[98] Indexing the World Wide Web: The Journey So Far: https://research.google.com/pubs/pub37043.html
[99] здесь — в формате PDF: https://pdfs.semanticscholar.org/28d8/288bff1b1fc693e6d80c238de9fe8b5e8160.pdf
[100] Why Writing Your Own Search Engine is Hard: http://queue.acm.org/detail.cfm?id=988407
[101] https://github.com/harpribot/awesome-information-retrieval: https://github.com/harpribot/awesome-information-retrieval
[102] Отличный блог: https://medium.com/@dtunkelang
[103] Daniel Tunkelang: https://www.cs.cmu.edu/~quixote/
[104] оценке поисковой системы: https://web.stanford.edu/class/cs276/handouts/lecture8-evaluation_2014-one-per-page.pdf
[105] локализацией игр: https://alconost.com/services/game-localization?utm_source=habrahabr&utm_medium=article&utm_campaign=translation&utm_content=about-search
[106] приложений и сайтов: https://alconost.com/services/software-localization?utm_source=habrahabr&utm_medium=article&utm_campaign=translation&utm_content=about-search
[107] рекламные и обучающие видеоролики: https://alconost.com/services/video-production?utm_source=habrahabr&utm_medium=article&utm_campaign=translation&utm_content=about-search
[108] Источник: https://habrahabr.ru/post/339894/
Нажмите здесь для печати.