Векторное представление материалов
Обычный поиск сопоставлял слова и известные варианты. Свободный вопрос может описывать задачу другими словами: «получить данные с сервера» вместо названия Fetch. Теперь рассмотрим представление текста числовым вектором, которое позволяет искать по близости таких представлений.
Начнём с подготовки документов. Векторный блок дополнительный: он требует модели и отдельных ресурсов, но не заменяет уже работающий точный поиск. Результатом урока станет файл векторов с явно записанными версиями корпуса, модели и способа подготовки текста.
Что обозначает вектор
Вектор — массив чисел фиксированной длины. Модель преобразует текст в такое представление; дальнейшая программа сравнивает его с представлениями других текстов. Близость может помочь найти связанные по смыслу материалы, но не является доказательством правильного ответа.
Одна координата обычно не имеет понятной подписи вроде «степень знания XML». Не стоит объяснять каждое число как отдельный человеческий признак без основания. Для приложения важны одинаковое пространство представлений и согласованный способ сравнения.
Вектор запроса и документа должны происходить из совместимого преобразования. Нельзя смешать несколько моделей только потому, что у них одинаковая длина массива. Размерность сама по себе не делает координаты сопоставимыми.
Смена модели или подготовки текста требует новой версии представлений. Иначе сервис будет сравнивать новый запрос со старым пространством документов. Внешне API продолжит возвращать числа, но их смысл окажется нарушен.
Модель для учебного примера
Используем локальную Sentence Transformers модель paraphrase-multilingual-MiniLM-L12-v2. В её карточке указано представление из 384 координат и многоязычное применение. Это подходящий предмет для эксперимента, а не обещание лучшего результата на всей русской технической библиотеке. Карточка модели.
Закрепим ревизию, указанную в метаданных репозитория на дату подготовки курса. Так будущая версия с тем же именем не будет молча принята за прежнюю. Метаданные модели.
Создайте model_config.py:
from pathlib import Path
from sentence_transformers import SentenceTransformer
ROOT = Path(__file__).resolve().parent
MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
REVISION = "e8f8c211226b894fcb81acc59f3b34ba3efd5f42"
POLICY = "title-headings-first80words-v1"
def load_model():
return SentenceTransformer(
MODEL, revision=REVISION, trust_remote_code=False,
cache_folder=str(ROOT / ".model-cache"),
)
def document_input(doc):
words = " ".join([doc["title"], *doc["headings"], doc["text"]]).split()
return " ".join(words[:80])
Функция load_model при первом выполнении загружает файлы модели в папку учебного проекта. Само наличие программы не означает, что веса уже скачаны. Для практики нужны совместимые зависимости, доступ к файлам модели и достаточные ресурсы.
Смысловой поиск не требует стороннего облачного API в этом примере. Однако загрузка весов является отдельным сетевым действием, а вычисление требует памяти и процессорного времени. Локальная модель не делает эти затраты нулевыми.
Какой текст преобразовывать
В document_input объединяются название, подзаголовки и первые 80 слов. Такое ограничение выбрано для маленькой библиотеки и удобного разбора. Оно не позволяет считать, что вектор описывает весь длинный урок одинаково подробно.
Модель имеет собственный предел токенов. Восемьдесят слов не гарантируют восемьдесят токенов: технические обозначения и русские слова могут разбиваться на части. Поэтому при самостоятельной практике изучите tokenizer и max_seq_length, а не полагайтесь только на число пробелов.
Для длинных материалов часто полезнее разделить текст на смысловые фрагменты. Тогда каждый фрагмент получает собственное представление и сохраняет ссылку на родительскую статью. Это усложняет группировку результатов, поэтому наша первая версия остаётся документной.
Выбор заголовка и начала текста — редакционная гипотеза. Если важная задача объясняется в последнем разделе, она может слабо повлиять на представление. Контрольные запросы должны выявлять такие случаи.
Получение файла
Создайте embed.py. Он читает тот же manifest, что использовался для серверного индекса, и записывает vectors.json рядом с программой, вне публичной папки.
from pathlib import Path
import hashlib
import json
import math
import re
from model_config import MODEL, REVISION, POLICY, load_model, document_input
ROOT = Path(__file__).resolve().parent
def main():
pointer = json.loads((ROOT / "public" / "manifest.json").read_text(encoding="utf-8"))
if not re.fullmatch(r"documents-[a-f0-9]{16}\.json", pointer["file"]):
raise ValueError("Неверный manifest")
payload = json.loads((ROOT / "public" / pointer["file"]).read_text(encoding="utf-8"))
if payload["schemaVersion"] != 1 or payload["version"] != pointer["version"]:
raise ValueError("Несогласованный корпус")
documents = payload["documents"]
if not documents:
raise ValueError("Пустой корпус")
model = load_model()
vectors = model.encode(
[document_input(doc) for doc in documents],
normalize_embeddings=True,
convert_to_numpy=True,
).tolist()
dimensions = len(vectors[0])
if dimensions != 384 or any(len(v) != dimensions or
not all(math.isfinite(x) for x in v) for v in vectors):
raise ValueError("Неверные векторы")
identity = {"model": MODEL, "revision": REVISION, "policy": POLICY,
"dimensions": dimensions}
embedding_version = hashlib.sha256(
json.dumps(identity, sort_keys=True).encode("utf-8")
).hexdigest()[:16]
output = {
"corpusVersion": payload["version"],
"embeddingVersion": embedding_version,
**identity,
"documents": [{**doc, "vector": vector}
for doc, vector in zip(documents, vectors)],
}
(ROOT / "vectors.json").write_text(
json.dumps(output, ensure_ascii=False, indent=2), encoding="utf-8"
)
if __name__ == "__main__":
main()
normalize_embeddings=True нормирует полученные векторы. Это позволит использовать скалярное произведение для сравнения в следующем уроке. Назначение параметра и метода encode описано в справке SentenceTransformer.
Файл сохраняет исходные ID, URL и поля документов. Вектор добавляется к записи, а не заменяет её текст. Карточку и ссылку по-прежнему можно построить из обычных данных.
Программа проверяет размерность и конечность чисел. JSON с NaN или массивом неожиданной длины не следует считать пригодным только потому, что запись на диск завершилась. Для более широкой практики добавьте контроль нормы и наблюдение за пакетной обработкой.
Версия представлений
В corpusVersion записана версия текста, а в embeddingVersion — идентичность модели и политики преобразования. Это два разных источника изменения. Новая статья меняет корпус; новая модель меняет способ представления всего корпуса.
Ревизия модели не гарантирует побитовое равенство результатов на любой аппаратуре и версиях вычислительных библиотек. Для воспроизводимого эксперимента полезно сохранять также окружение и параметры запуска. В нашем файле закреплена смысловая совместимость, а не обещание идентичных плавающих чисел.
При изменении политики с 80 слов на несколько фрагментов нужно изменить POLICY и пересчитать представления. Нельзя просто оставить старое имя, чтобы не менять остальную инфраструктуру.
Если документ удалён, полная подготовка из нового manifest исключает его. При инкрементальном обновлении потребовалась бы отдельная операция удаления. Само создание новых векторов не убирает все старые записи автоматически.
Ожидаемое наблюдение
После самостоятельного выполнения файл должен содержать шесть документов и по 384 числа у каждого. Это проверяемая форма результата. Конкретные значения и качество ближайших материалов появляются только после реального вычисления и оценки.
Не подставляйте случайные массивы вместо настоящих результатов модели и не называйте их смысловыми представлениями. Маленькие условные векторы полезны для объяснения геометрии, но имеют другое назначение.
Пока мы не знаем, лучше ли модель отвечает на вопрос о получении данных с сервера. Такая гипотеза будет проверяться через сравнение ближайших документов и прежние контрольные запросы. Размерность 384 сама по себе ничего не говорит о полезности ответа.
Сохранённые векторы не являются независимым исходником статьи. Их можно восстановить из опубликованного корпуса, закреплённой модели и политики подготовки текста. Поэтому изменение Markdown требует обновления производных данных; ручное редактирование отдельных чисел нарушило бы воспроизводимость. Сначала исправляйте содержательный источник, затем повторяйте вычисление нужной версии.
Теперь у документов есть совместимое числовое представление и запись его происхождения. Следующий урок преобразует запрос той же моделью, рассчитает близость и покажет границу между ближайшим материалом и действительно подходящим.