Улучшение RAG с помощью графов знаний. graphrag.. graphrag. knowledge graph.. graphrag. knowledge graph. llm.. graphrag. knowledge graph. llm. milvus.. graphrag. knowledge graph. llm. milvus. rag.

Знакомство с RAG и связанными с ним проблемами

Генерация с дополненной выборкой (RAG) — это метод, который соединяет внешние источники данных для улучшения вывода больших языковых моделей (LLM). Этот метод идеально подходит для LLM для доступа к частным или специфичным для предметной области данным и решения проблем, связанных с галлюцинациями. Поэтому RAG широко используется для поддержки многих приложений GenAI, таких как чат-боты AI и системы рекомендаций.

Базовый RAG обычно объединяет векторную базу данных и LLM, где векторная база данных хранит и извлекает контекстную информацию для пользовательских запросов, а LLM генерирует ответы на основе извлеченного контекста. Этот подход хорошо работает во многих случаях, однако он испытывает трудности со сложными задачами, такими как многоадресное рассуждение или ответы на вопросы, требующие соединения разрозненных фрагментов информации.

Например, вопрос «Какое имя было дано сыну человека, который победил узурпатора Аллектуса?»

Для ответа на этот вопрос базовый RAG обычно выполняет следующие шаги:

  1. Определяет человека: определяет, кто победил Аллектуса.

  2. Определяет сына человека: ищет информацию о семье этого человека, в частности о его сыне.

  3. Ищет имя: определяет имя сына.

Проблема обычно возникает на первом этапе, поскольку базовый RAG извлекает текст на основе семантического сходства, а не напрямую отвечает на сложные запросы, где конкретные детали могут не быть упомянуты в наборе данных. Это ограничение затрудняет поиск точной необходимой информации, часто требуя дорогостоящих и непрактичных решений, таких как ручное создание пар вопросов и ответов для частых запросов.

Для решения таких проблем Microsoft Research представила GraphRAG, совершенно новый метод, который дополняет извлечение и генерацию RAG с помощью графов знаний. В следующих разделах мы объясним, как работает GraphRAG и как запустить его с векторной базой данных Milvus.

Что такое GraphRAG и как он работает?

В отличие от базового RAG, который использует векторную базу данных для извлечения семантически похожего текста, GraphRAG улучшает RAG, включая графы знаний (KG). Графы знаний — это структуры данных, которые хранят и связывают данные на основе их отношений.

Конвейер GraphRAG обычно состоит из двух основных процессов: индексации и обработки запросов.

Конвейер GraphRAG (Источник изображения: документ GraphRAG)

Конвейер GraphRAG (Источник изображения: документ GraphRAG)

Индексация

Процесс индексации состоит из четырех основных этапов:

  1. Сегментация текстовых единиц: весь входной корпус делится на несколько текстовых единиц (текстовых фрагментов). Эти фрагменты являются наименьшими анализируемыми единицами и могут быть абзацами, предложениями или другими логическими единицами. Сегментируя длинные документы на более мелкие фрагменты, мы можем извлекать и сохранять более подробную информацию об этих входных данных.

  2. Извлечение сущностей, связей и утверждений: GraphRAG использует LLM для идентификации и извлечения всех сущностей (имен людей, мест, организаций и т. д.), связей между ними и ключевых утверждений, выраженных в тексте из каждой текстовой единицы. Мы будем использовать эту извлеченную информацию для построения начального графа знаний.

  3. Иерархическая кластеризация: GraphRAG использует алгоритм Лейдена для выполнения иерархической кластеризации на начальных графах знаний. Лейден — это алгоритм обнаружения сообществ, который может эффективно обнаруживать структуры сообществ в графе. Сущности в каждом кластере назначаются разным сообществам для более глубокого анализа.

Примечание: сообщество — это группа узлов в графе, которые плотно связаны друг с другом, но слабо связаны с другими плотными группами в сети.

  1. Генерация сводки сообщества: GraphRAG генерирует сводки для каждого сообщества и его участников, используя подход «снизу вверх». Эти сводки включают основные сущности в сообществе, их отношения и ключевые утверждения. Этот шаг дает обзор всего набора данных и предоставляет полезную контекстную информацию для последующих запросов.

Рисунок 1: Граф знаний, созданный LLM и построенный с использованием GPT-4 Turbo. (Источник изображения: Microsoft Research)

Рисунок 1: Граф знаний, созданный LLM и построенный с использованием GPT-4 Turbo. (Источник изображения: Microsoft Research)

Обработка запросов

GraphRAG имеет два различных рабочих процесса обработки запросов, адаптированных для разных запросов.

  • Глобальный поиск для рассуждений о целостных вопросах, связанных со всем корпусом данных, путем использования сводок сообщества.

  • Локальный поиск для рассуждений о конкретных сущностях путем распространения на их соседей и связанных с этим концепциях.

Этот рабочий процесс глобального поиска включает следующие фазы.

Рисунок 2: Глобальный поток данных поиска (Источник изображения: Microsoft Research)

Рисунок 2: Глобальный поток данных поиска (Источник изображения: Microsoft Research)
  1. История запросов и диалогов пользователей: система принимает историю запросов и разговоров пользователей в качестве начальных входных данных.

  2. Пакеты отчетов о сообществах: в качестве контекстных данных система использует отчеты о сообществах узлов, сгенерированных LLM из указанного уровня иерархии сообщества. Эти отчеты сообщества перемешиваются и делятся на несколько пакетов (Пакет 1, Пакет 2… Пакет N).

  3. RIR (Оцененные промежуточные ответы): Каждый пакет отчетов сообщества далее делится на текстовые фрагменты предопределенного размера. Каждый текстовый фрагмент используется для генерации промежуточного ответа. Ответ содержит список информационных фрагментов, называемых пунктами. Каждый пункт имеет числовую оценку, указывающую ее важность. Эти сгенерированные промежуточные ответы являются Оцененными промежуточными ответами (Ответ 1, Ответ 2… Ответ N).

  4. Ранжирование и фильтрация: система ранжирует и фильтрует эти промежуточные ответы, выбирая наиболее важные пункты. Выбранные важные пункты формируют Агрегированные промежуточные ответы.

  5. Окончательный ответ: Агрегированные промежуточные ответы используются в качестве контекста для генерации окончательного ответа.

Когда пользователи задают вопросы о конкретных сущностях (например, имена людей, места, организации и т. д.), мы рекомендуем использовать процесс локального поиска. Этот процесс включает следующие шаги:

Рисунок 3: Поток данных локального поиска (Источник изображения: Microsoft Research)

Рисунок 3: Поток данных локального поиска (Источник изображения: Microsoft Research)
  1. Запрос пользователя: Сначала система получает запрос пользователя, который может быть простым вопросом или более сложным запросом.

  2. Поиск похожих сущностей: система идентифицирует набор сущностей из графа знаний, которые семантически связаны с пользовательским вводом. Эти сущности служат точками входа в граф знаний. На этом этапе для проведения поиска по схожести текста используется векторная база данных, например, Milvus.

  3. Сопоставление сущности и текстовых единиц: извлеченные текстовые единицы сопоставляются с соответствующими сущностями, удаляя исходную текстовую информацию.

  4. Извлечение сущностей и связей: на этом этапе извлекается конкретная информация о сущностях и их соответствующих связях.

  5. Сопоставление сущностей с ковариатами: на этом этапе сущности сопоставляются с их ковариатами, которые могут включать статистические данные или другие соответствующие атрибуты.

  6. Сопоставление сущностей с отчетами о сообществах: отчеты сообщества интегрируются в результаты поиска, включая некоторую глобальную информацию.

  7. Использование истории диалогов: система использует историю диалогов, чтобы лучше понять намерение и контекст пользователя.

  8. Генерация ответа: система конструирует и отвечает на запрос пользователя на основе отфильтрованных и отсортированных данных, которые были сгенерированы на предыдущих этапах.

Сравнение базового RAG и GraphRAG по качеству выходных данных

Чтобы продемонстрировать эффективность GraphRAG, его создатели сравнили качество выходных данных базового RAG и GraphRAG в своем блоге. Для иллюстрации я приведу простой пример.

Используемый набор данных

Для своих экспериментов создатели GraphRAG использовали набор данных «Информация о насильственных инцидентах из новостных статей» (VIINA).

Примечание: этот набор данных содержит деликатные темы. Он был выбран исключительно из-за своей сложности и наличия различных мнений и частичной информации.

Обзор эксперимента

Базовому RAG и GraphRAG задавали один и тот же вопрос, который для составления ответа требует агрегации информации по всему набору данных.

Вопрос: Каковы 5 основных тем в наборе данных?

Ответы показаны на изображении ниже. Результаты базового RAG не имели отношения к военной тематике, поскольку векторный поиск давал несвязанный текст, что приводило к неточной оценке. Напротив, GraphRAG предоставил четкий и релевантный ответ, определив основные темы и вспомогательные детали. Результаты были согласованы с набором данных со ссылками на исходный материал.

Рисунок 4: Базовый RAG по сравнению с GraphRAG при ответе на сложные вопросы

Рисунок 4: Базовый RAG по сравнению с GraphRAG при ответе на сложные вопросы

Дальнейшие эксперименты, приведенные в статье «От локального к глобальному: подход Graph RAG к резюмированию, ориентированному на запросы», показали, что GraphRAG значительно улучшает многоадресное рассуждение и сложное резюмирование информации. Исследование показало, что GraphRAG превосходит базовый RAG как по полноте, так и по разнообразию:

  • Полнота: степень, в которой ответ охватывает все аспекты вопроса.

  • Разнообразие: разнообразие и богатство точек зрения и идей, которые содержатся в ответе.

Для получения более подробной информации об этих экспериментах мы рекомендуем вам прочитать оригинальную статью GraphRAG.

Как реализовать GraphRAG с векторной базой данных Milvus

GraphRAG расширяет приложения RAG с помощью графов знаний, а также использует векторную базу данных для извлечения соответствующих сущностей. В этом разделе показано, как реализовать GraphRAG, создать индекс GraphRAG и запросить его с помощью векторной базы данных Milvus.

Предварительные условия

Перед запуском кода убедитесь, что вы установили следующие зависимости:

pip install --upgrade pymilvus
pip install git+https://github.com/zc277584121/graphrag.git

Примечание: мы установили GraphRAG из форк репозитория, поскольку на момент написания статьи функция хранения Milvus все еще ожидает официального слияния.

Начнем с рабочего процесса индексации.

Подготовка данных

Загрузите из Project Gutenberg небольшой текстовый файл примерно с тысячей строк и используйте его для индексации GraphRAG.

Этот набор данных посвящен истории Леонардо да Винчи. Мы используем GraphRAG для построения графического индекса всех связей, касающихся да Винчи, и векторной базой данных Milvus для поиска соответствующих знаний для ответа на вопросы.

import nest_asyncio
nest_asyncio.apply()
import os
import urllib.request
index_root = os.path.join(os.getcwd(), 'graphrag_index')
os.makedirs(os.path.join(index_root, 'input'), exist_ok=True)
url = "https://www.gutenberg.org/cache/epub/7785/pg7785.txt"
file_path = os.path.join(index_root, 'input', 'davinci.txt')
urllib.request.urlretrieve(url, file_path)
with open(file_path, 'r+', encoding='utf-8') as file:
    # We use the first 934 lines of the text file, because the later lines are not relevant for this example.
    # If you want to save api key cost, you can truncate the text file to a smaller size.
    lines = file.readlines()
    file.seek(0)
    file.writelines(lines[:934])  # Decrease this number if you want to save api key cost.
    file.truncate()

Инициализация рабочей области

Теперь давайте используем GraphRAG для индексации текстового файла. Чтобы инициализировать рабочую область, сначала запустим команду graphrag.index --init.

python -m graphrag.index --init --root ./graphrag_index

Настройка файла env и параметров

Вы найдете файл .env в корневом каталоге индекса. Чтобы использовать его, добавьте свой ключ API OpenAI в файл .env.

Важные примечания:

  • В этом примере мы будем использовать модели OpenAI, поэтому убедитесь, что у вас есть готовый ключ API.

  • Индексация GraphRAG требует больших затрат, поскольку она обрабатывает весь текстовый корпус с помощью LLM. Чтобы сэкономить деньги, попробуйте сократить текстовый файл до меньшего размера.

Запуск конвейера индексации

Процесс индексации займет некоторое время. После завершения в папке ./graphrag_index/output/<timestamp>/появится новая директория с артефактами, содержащими серию файлов в формате Parquet.

python -m graphrag.index --root ./graphrag_index

Обработка запросов с помощью векторной базы данных Milvus

На этапе выполнения запросов мы используем Milvus для хранения эмбеддингов описаний сущностей, необходимых для локального поиска в GraphRAG. Этот подход объединяет структурированные данные из графа знаний с неструктурированными данными из входных документов, что позволяет дополнить контекст LLM релевантной информацией о сущностях и обеспечить более точные ответы.

import os
import pandas as pd
import tiktoken
from graphrag.query.context_builder.entity_extraction import EntityVectorStoreKey
from graphrag.query.indexer_adapters import (
    # read_indexer_covariates,
    read_indexer_entities,
    read_indexer_relationships,
    read_indexer_reports,
    read_indexer_text_units,
)
from graphrag.query.input.loaders.dfs import (
    store_entity_semantic_embeddings,
)
from graphrag.query.llm.oai.chat_openai import ChatOpenAI
from graphrag.query.llm.oai.embedding import OpenAIEmbedding
from graphrag.query.llm.oai.typing import OpenaiApiType
from graphrag.query.question_gen.local_gen import LocalQuestionGen
from graphrag.query.structured_search.local_search.mixed_context import (
    LocalSearchMixedContext,
)
from graphrag.query.structured_search.local_search.search import LocalSearch
from graphrag.vector_stores import MilvusVectorStore
output_dir = os.path.join(index_root, "output")
subdirs = [os.path.join(output_dir, d) for d in os.listdir(output_dir)]
latest_subdir = max(subdirs, key=os.path.getmtime)  # Get latest output directory
INPUT_DIR = os.path.join(latest_subdir, "artifacts")
COMMUNITY_REPORT_TABLE = "create_final_community_reports"
ENTITY_TABLE = "create_final_nodes"
ENTITY_EMBEDDING_TABLE = "create_final_entities"
RELATIONSHIP_TABLE = "create_final_relationships"
COVARIATE_TABLE = "create_final_covariates"
TEXT_UNIT_TABLE = "create_final_text_units"
COMMUNITY_LEVEL = 2

Загрузка данных из процесса индексации

В процессе индексации будет сгенерировано несколько файлов parquet. Мы загружаем их в память и сохраняем информацию об описании сущностей в векторной базе данных Milvus.

Чтение сущностей:

# read nodes table to get community and degree data
entity_df = pd.read_parquet(f"{INPUT_DIR}/{ENTITY_TABLE}.parquet")
entity_embedding_df = pd.read_parquet(f"{INPUT_DIR}/{ENTITY_EMBEDDING_TABLE}.parquet")
entities = read_indexer_entities(entity_df, entity_embedding_df, COMMUNITY_LEVEL)
description_embedding_store = MilvusVectorStore(
    collection_name="entity_description_embeddings",
)
# description_embedding_store.connect(uri="http://localhost:19530") # For Milvus docker service
description_embedding_store.connect(uri="./milvus.db") # For Milvus Lite
entity_description_embeddings = store_entity_semantic_embeddings(
    entities=entities, vectorstore=description_embedding_store
)
print(f"Entity count: {len(entity_df)}")
entity_df.head()

Количество сущностей: 651

Рисунок 5: Снимок экрана сущностей

Рисунок 5: Снимок экрана сущностей

Чтение связей

relationship_df = pd.read_parquet(f"{INPUT_DIR}/{RELATIONSHIP_TABLE}.parquet")
relationships = read_indexer_relationships(relationship_df)
print(f"Relationship count: {len(relationship_df)}")
relationship_df.head()

Количество связей: 290

Рисунок 6: Снимок экрана связей

Рисунок 6: Снимок экрана связей

Чтение отчетов сообщества

report_df = pd.read_parquet(f"{INPUT_DIR}/{COMMUNITY_REPORT_TABLE}.parquet")
reports = read_indexer_reports(report_df, entity_df, COMMUNITY_LEVEL)
print(f"Report records: {len(report_df)}")
report_df.head()

Записи отчетов: 45

Рисунок 7: Снимок экрана записей отчетов

Рисунок 7: Снимок экрана записей отчетов

Чтение текстовых единиц

text_unit_df = pd.read_parquet(f"{INPUT_DIR}/{TEXT_UNIT_TABLE}.parquet")
text_units = read_indexer_text_units(text_unit_df)
print(f"Text unit records: {len(text_unit_df)}")
text_unit_df.head()

Записи текстовых единиц: 51

Рисунок 8: Снимок экрана записей текстовых единиц

Рисунок 8: Снимок экрана записей текстовых единиц

Создание локальной поисковой системы

Мы подготовили необходимые данные для локальной поисковой системы. Теперь мы можем создать с их помощью экземпляр LocalSearch, LLM и модель эмбеддингов.

api_key = os.environ["OPENAI_API_KEY"]  # Your OpenAI API key
llm_model = "gpt-4o"  # Or gpt-4-turbo-preview
embedding_model = "text-embedding-3-small"
llm = ChatOpenAI(
    api_key=api_key,
    model=llm_model,
    api_type=OpenaiApiType.OpenAI,
    max_retries=20,
)
token_encoder = tiktoken.get_encoding("cl100k_base")
text_embedder = OpenAIEmbedding(
    api_key=api_key,
    api_base=None,
    api_type=OpenaiApiType.OpenAI,
    model=embedding_model,
    deployment_name=embedding_model,
    max_retries=20,
)
context_builder = LocalSearchMixedContext(
    community_reports=reports,
    text_units=text_units,
    entities=entities,
    relationships=relationships,
    covariates=None, #covariates,#todo
    entity_text_embeddings=description_embedding_store,
    embedding_vectorstore_key=EntityVectorStoreKey.ID,  # if the vectorstore uses entity title as ids, set this to EntityVectorStoreKey.TITLE
    text_embedder=text_embedder,
    token_encoder=token_encoder,
)
local_context_params = {
    "text_unit_prop": 0.5,
    "community_prop": 0.1,
    "conversation_history_max_turns": 5,
    "conversation_history_user_turns_only": True,
    "top_k_mapped_entities": 10,
    "top_k_relationships": 10,
    "include_entity_rank": True,
    "include_relationship_weight": True,
    "include_community_rank": False,
    "return_candidate_context": False,
    "embedding_vectorstore_key": EntityVectorStoreKey.ID,  # set this to EntityVectorStoreKey.TITLE if the vectorstore uses entity title as ids
    "max_tokens": 12_000,  # change this based on the token limit you have on your model (if you are using a model with 8k limit, a good setting could be 5000)
}
llm_params = {
    "max_tokens": 2_000,  # change this based on the token limit you have on your model (if you are using a model with 8k limit, a good setting could be 1000=1500)
    "temperature": 0.0,
}
search_engine = LocalSearch(
    llm=llm,
    context_builder=context_builder,
    token_encoder=token_encoder,
    llm_params=llm_params,
    context_builder_params=local_context_params,
    response_type="multiple paragraphs",  # free form text describing the response type and format, can be anything, e.g. prioritized list, single paragraph, multiple paragraphs, multiple-page report
)

Создание запроса

result = await search_engine.asearch("Tell me about Leonardo Da Vinci")
print(result.response)
# Leonardo da Vinci
    Leonardo da Vinci, born in 1452 in the town of Vinci near Florence, is widely celebrated as one of the most versatile geniuses of the Italian Renaissance. His full name was Leonardo di Ser Piero d'Antonio di Ser Piero di Ser Guido da Vinci, and he was the natural and first-born son of Ser Piero, a country notary [Data: Entities (0)]. Leonardo's contributions spanned various fields, including art, science, engineering, and philosophy, earning him the title of the most Universal Genius of Christian times [Data: Entities (8)].
    ## Early Life and Training
    Leonardo's early promise was recognized by his father, who took some of his drawings to Andrea del Verrocchio, a renowned artist and sculptor. Impressed by Leonardo's talent, Verrocchio accepted him into his workshop around 1469-1470. Here, Leonardo met other notable artists, including Botticelli and Lorenzo di Credi [Data: Sources (6, 7)]. By 1472, Leonardo was admitted into the Guild of Florentine Painters, marking the beginning of his professional career [Data: Sources (7)].
    ## Artistic Masterpieces
    Leonardo is perhaps best known for his iconic paintings, such as the "Mona Lisa" and "The Last Supper." The "Mona Lisa," renowned for its subtle expression and detailed background, is housed in the Louvre and remains one of the most famous artworks in the world [Data: Relationships (0, 45)]. "The Last Supper," a fresco depicting the moment Jesus announced that one of his disciples would betray him, is located in the refectory of Santa Maria delle Grazie in Milan [Data: Sources (2)]. Other significant works include "The Virgin of the Rocks" and the "Treatise on Painting," which he began around 1489-1490 [Data: Relationships (7, 12)].
    ## Scientific and Engineering Contributions
    Leonardo's genius extended beyond art to various scientific and engineering endeavors. He made significant observations in anatomy, optics, and hydraulics, and his notebooks are filled with sketches and ideas that anticipated many modern inventions. For instance, he anticipated Copernicus' theory of the earth's movement and Lamarck's classification of animals [Data: Relationships (38, 39)]. His work on the laws of light and shade and his mastery of chiaroscuro had a profound impact on both art and science [Data: Sources (45)].
    ## Patronage and Professional Relationships
    Leonardo's career was significantly influenced by his patrons. Ludovico Sforza, the Duke of Milan, employed Leonardo as a court painter and general artificer, commissioning various works and even gifting him a vineyard in 1499 [Data: Relationships (9, 19, 84)]. In his later years, Leonardo moved to France under the patronage of King Francis I, who provided him with a princely income and held him in high regard [Data: Relationships (114, 37)]. Leonardo spent his final years at the Manor House of Cloux near Amboise, where he was frequently visited by the King and supported by his close friend and assistant, Francesco Melzi [Data: Relationships (28, 122)].
    ## Legacy and Influence
    Leonardo da Vinci's influence extended far beyond his lifetime. He founded a School of painting in Milan, and his techniques and teachings were carried forward by his students and followers, such as Giovanni Ambrogio da Predis and Francesco Melzi [Data: Relationships (6, 15, 28)]. His works continue to be celebrated and studied, cementing his legacy as one of the greatest masters of the Renaissance. Leonardo's ability to blend art and science has left an indelible mark on both fields, inspiring countless generations of artists and scientists [Data: Entities (148, 86); Relationships (27, 12)].
    In summary, Leonardo da Vinci's unparalleled contributions to art, science, and engineering, combined with his innovative thinking and profound influence on his contemporaries and future generations, make him a towering figure in the history of human achievement. His legacy continues to inspire admiration and study, underscoring the timeless relevance of his genius.

Результаты GraphRAG конкретны, с четко обозначенными цитируемыми источниками данных.

Генерация вопросов

GraphRAG также может генерировать вопросы на основе исторических запросов, что полезно для создания рекомендуемых вопросов в диалоге чат-бота. Этот метод объединяет структурированные данные из графа знаний с неструктурированными данными из входных документов для создания потенциальных вопросов, связанных с определенными сущностями.

question_generator = LocalQuestionGen(
   llm=llm,
   context_builder=context_builder,
   token_encoder=token_encoder,
   llm_params=llm_params,
   context_builder_params=local_context_params,
)
question_history = [
    "Tell me about Leonardo Da Vinci",
    "Leonardo's early works",
]

Генерация вопросов на основе истории.

candidate_questions = await question_generator.agenerate(
        question_history=question_history, context_data=None, question_count=5
    )
candidate_questions.response
["- What were some of Leonardo da Vinci's early works and where are they housed?",
     "- How did Leonardo da Vinci's relationship with Andrea del Verrocchio influence his early works?",
     '- What notable projects did Leonardo da Vinci undertake during his time in Milan?',
     "- How did Leonardo da Vinci's engineering skills contribute to his projects?",
     "- What was the significance of Leonardo da Vinci's relationship with Francis I of France?"]

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

# import shutil
#
# shutil.rmtree(index_root)

Резюме

В этой статье мы изучили GraphRAG, инновационный метод, который улучшает технологию RAG за счет интеграции графов знаний. GraphRAG идеально подходит для решения сложных задач, таких как многошаговое рассуждение и ответы на комплексные вопросы, требующие связывания разрозненных фрагментов информации.

В сочетании с векторной базой данных Milvus, GraphRAG может эффективно анализировать сложные семантические связи в больших наборах данных, предоставляя более точные и глубокие результаты. Эта мощная комбинация делает GraphRAG незаменимым инструментом для различных практических приложений в области генеративного ИИ, предоставляя надежное решение для понимания и обработки сложной информации.

Автор: kucev

Источник

Rambler's Top100