Привет, Хабр! В предыдущем материале мы упомянули, что при работе с текстовыми корпусами embedding-модели не всегда оптимальный инструмент. В этой публикации на примере задачи поиска релевантных документов по запросу рассмотрим ограничения такого варианта решения, разберем на практике гибридный подход и оценим его эффективность.

Меня зовут Вадим Скляров, я аналитик компании MWS, и уже по традиции мы будем разбираться в технической задаче с позиции системного и бизнес-анализа:

  • сформулируем основные моменты, которые нужно знать и описать, прежде чем передать проект команде разработки;

  • рассмотрим, как быстро проверить подходы к решению.


Почему embedding-модели не являются волшебной таблеткой в задачах поиска похожих документов

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

Если за основу поиска принять только сравнение embeddings, результатом такого подхода будет список всех документов поисковой базы, отсортированный по убыванию косинусного сходства. Сперва решение задачи выглядит тривиально: нужно выбрать N первых документов, косинусное сходство которых больше минимального значения релевантности. На этом шаге аналитик должен ответить на вопрос:

Какой должна быть нижняя оценка релевантности?

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

Приведем пример

Исходный документ. Шотландии принадлежит более 790 островов, из которых около 130 постоянно населены людьми. Самые крупные острова: Льюис и Харрис (2179 км²), которые технически считаются одним островом во Внешних Гебридах; остров Скай (1656 км²) — во Внутренних Гебридах; Мейнленд (969 км²) — на Шетландских островах. Шотландские острова географически разделены на четыре основные группы: Внутренние Гебриды, Внешние Гебриды, Оркнейские острова и Шетландские острова. Каждый архипелаг обладает уникальной историей: Оркнейские и Шетландские острова принадлежат Шотландии с 1472 года, когда были переданы в качестве приданого, а Гебридские острова окончательно вошли в состав королевства после Пертского договора 1266 года. На островах проживает около 103 тысяч человек, сохраняющих особую островную идентичность и во многих местах гэльский язык.

Оценим его косинусное сходство для нескольких запросов с одинаковым смыслом (векторизация выполнена с помощью embedding-модели LaBSE).

Запрос

Косинусное сходство

1

Сколько островов принадлежит Шотландии и сколько человек на них проживает?

0,6411

2

Сколько островов у Шотландии? Сколько человек там проживает?

0,5900

3

Сколько островов у Шотландии?

0,5805

4

Сколько островов принадлежит Люксембургу и сколько человек на них проживает?

0,5547

Если оценить нижнюю границу как 0,6, то исходный документ будет считаться релевантным только для первого запроса, что с точки зрения бизнес-логики, несомненно, ошибка, поскольку для второго и третьего исходник содержит необходимый ответ. 

Если понизить границу, например, до 0,55, исходный документ будет считаться релевантным для всех запросов, в том числе для четвертого, что тоже ошибка.

Можно, конечно, провести серию экспериментов и опытным путем определить требуемое значение нижней границы. Но в этом случае рано или поздно придется отвечать на вопрос бизнеса: «Почему документ D не попал в результаты выдачи по запросу Q?» И четкого ответа, скорее всего, не будет.

Гибридный подход — одно из решений, которое позволяет эффективнее справиться с данной задачей. Его реализация может быть выполнена разными способами: 

  1. С применением лексического анализа. Используется лингвистическая модель «мешок слов» (BoW), которая на основании извлеченных лексических признаков документов формирует альтернативный список кандидатов на релевантность. Затем векторный и лексический списки анализируются и составляется итоговый список.

  2. С использованием LLM-модели кросс-энкодера. Алгоритмами векторного или лексического поиска формируется список документов-кандидатов. Каждый вместе с запросом передается в модель кросс-энкодер, которая вычисляет оценку его релевантности.

Здесь мы рассмотрим первый способ. Он выглядит привлекательнее по следующим причинам:

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

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

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

Как лексический анализ помогает улучшить качество поиска релевантных документов

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

  1. Качество поиска будет выше, если сравнение запроса и документа выполнять не по словам, а по лексемам — нормальным формам слов, объединяющих все его словоформы. Например, «приехал», «приехали», «приезжали», «приедут» соответствуют одной лексеме — «приезжать». Для преобразования словоформ в лексемы разработаны разные алгоритмы, например: snowball, ispell и так далее.

  2. В текстах могут употребляться словосочетания, описывающие неделимую единицу информации, например «Нижний Новгород». Они называются токенами и при поиске учитываются как одно целое.

  3. Порядок лексем и токенов и расстояние между ними важны и влияют на качество поиска.

  4. Существуют семантические связи — например, у синонимов, аббревиатур, близких по смыслу слова. Их необходимо учитывать при поиске.

На основании этих идей в гибридном поиске применяются такие приемы лексического анализа: 

  • Выделение ключевых слов. Зная их, можно из списков релевантных текстов исключить те, где таких слов нет.

  • Извлечение лексических признаков. Специальные алгоритмы преобразуют произвольный текст в структурированный вид, а извлеченные признаки помогут отсортировать документы по степени релевантности запросу.

Фильтрация по ключевым словам

Одно из назначений ключевых слов — семантическое сжатие информации. Если обратиться к примеру выше и включить в список ключевых слов все географические названия, смыслы текста можно представить так: «этот текст про Шотландию», «этот текст про остров Льюис» и так далее. И тогда для поискового запроса «cколько островов принадлежит Люксембургу и сколько человек на них проживает?» исходный документ никогда не будет определен релевантным, какое бы значение косинусного сходства с запросом он ни имел.

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

  1. Выделение ключевых слов в тексте — эффективный инструмент предварительной фильтрации и помогает на первом этапе поиска исключить заведомо нерелевантные документы.

  2. Не во всех случаях это выделение возможно и оправданно.

  3. Аналитик должен оценить эффективность использования ключевых слов на проекте и сформулировать правила их применения в поиске.

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

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

  2. Определить синтаксическую конструкцию (шаблон), которой может соответствовать значительная доля всех возможных поисковых запросов. Вероятно, в новостной тематике большинство запросов будет посвящено поиску событий, для которых хорошо подойдет синтаксическая конструкция «субъект → действие → объект → время».

  3. Проанализировать, какие типы ключевых слов могут соответствовать элементам синтаксической конструкции. В нашем примере для субъекта, скорее всего, подойдут именованные сущности типа «персона, организация», а для объекта — именованная сущность типа «локация».

После таких рассуждений задача определения в тексте ключевых слов приобрела конкретную постановку. Выделение именованных сущностей хорошо реализовано для разных языков программирования: например, на Python она решается несколькими строками кода с помощью библиотеки spacy.

Сортируем документы по лингвистическим признакам

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

  • TF-IDF основан на анализе частоты и важности каждого слова;

  • BM25 расширяет алгоритм TF-IDF, дополнительно учитывая в качестве признака длину документа;

  • алгоритм Postgresql добавляет в анализ порядок слов.

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

В системах управления базами данных, как правило, нативно реализован один или несколько алгоритмов ранжирования хранимых документов по извлекаемым признакам. Например, в Postgresql вычисляется плотность покрытия, при этом используется метод, разработанный Кларком, Кормаком и Тадхоуп, а Elasticsearch по умолчанию применяет BM25. Поэтому выбор алгоритма на проекте тесно связан с архитектурным решением по организации хранения поисковых документов.

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

Уменьшаем эвристику определения релевантных документов

Алгоритм гибридного подхода с использованием лексического анализа документов следующий:

1. Выделяются два независимых списка кандидатов релевантных документов, полученных:

  • по результатам вычисления косинусного сходства — векторный поиск;

  • по ранжированию на основании лингвистических признаков.

Как правило, эти списки пересекаются, то есть содержат одинаковые документы на одних и тех же или разных позициях.

2. Списки объединяются, и для каждого документа по специальному алгоритму вычисляется его общая оценка (решается задача слияния рангов). 

3. Итоговое множество делится на два кластера — релевантные и нерелевантные документы. 

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

Для решения задачи слияния рангов можно использовать следующие алгоритмы:

  • Reciprocal Rank Fusion;

  • подсчет Борда;

  • слияние на основе оценок (Score-based Merging).

Для кластеризации документов можно поэкспериментировать с K-means и DBSCAN.

Ради справедливости стоит заметить, что данный подход неидеален: алгоритмы слияния рангов применяют эмпирические коэффициенты. Но способ вполне приемлем для решения многих практических задач.

Реализация гибридного подхода с использованием лексического анализа является для аналитика отправной точкой проектирования задач поиска релевантных документов.

Практический пример оценки эффективности гибридного поиска

Выбираем стек разработки

Для быстрой проверки нам понадобится:

  1. Размеченный датасет документов из предметной области проекта: в нем должны быть определены пары «запрос — релевантный ответ». Если повезет, готовый размеченный датасет нужной тематики удастся найти в свободном доступе. В этой статье в качестве демонстрации используем MTS-AI-SearchSkill/MTSBerquad.

  2. СУБД для хранения. Postgresql — хороший выбор, потому что в ней просто реализовать все требования к организации гибридного поиска:

    1. Для хранения векторов и оценки косинусного сходства можно использовать расширение pg_vector.

    2. Для лексического анализа подойдет встроенная функция полнотекстового поиска. При настройке конфигурации дополнительно нужно скачать ispell-словарь лемматизации для русского языка.

  3. Инструмент обработки документов датасета — используем Python:

    1. Для извлечения именованных сущностей в качестве ключевых слов — библиотеку spacy.

    2. Для векторизации текста — embedding-модель cointegrated/LaBSE-en-ru через обертку langchain_huggingface.

  4. Алгоритм RRF для задачи слияния рангов списков кандидатов в релевантные документы. Он использует только ранг (позицию) документа в каждом из списков-кандидатов и вычисляет итоговую оценку релевантности по формуле:

RRF Score = Σ (1 / (k + rank_i))

  • rank_i — ранг документа в i-том списке результатов;

  • k — константа, предотвращающая слишком большое влияние документов на самых верхних позициях (часто k = 60).

Инструмент кластеризации на релевантные и нерелевантные — используем k-means.

Планируем эксперимент

Выбранный в качестве примера датасет состоит из записей, каждая из которых включает:

1) вопрос — question;

2) текст, содержащий ответ на запрос, — context;

3) ответ на запрос — answer (для нашей задачи эта информация не пригодится).

Датасет включает дубликаты по полю context (один текст используется для ответов на несколько вопросов). После удаления дубликатов получаем связь один к одному: для каждого вопроса (question) существует только один релевантный документ (context). В этом случае оценка эффективности гибридного поиска может быть выполнена так:

  1. Формируется список поисковых запросов (requests), максимально близких по смыслу эталонному вопросу (произвольно выбранному из question датасета).

  2. Выполняются итерации гибридного поиска для каждого поискового запроса по документам ('context'). Результатом каждой итерации будет ограниченный список кандидатов в релевантные документы.

  3. Каждому документу из списка, полученного в шаге 2, соответствует единственный вопрос датасета. Анализируются ранги (положение) вопросов в списке и их score — оценка релевантности.

  4. Гибридный поиск считается эффективным, если:

    1. Во всех (в большинстве) итерациях поиска эталонный вопрос будет на первом месте поисковой выдачи.

    2. Значения score списка результатов поиска делятся на три непересекающихся кластера:

      1. Единственный релевантный документ, соответствующий эталонному вопросу.

      2. Группа документов, близких по смыслу эталонному вопросу (документы, которые частично отвечают на вопрос).

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

Пишем код

1. Создаем таблицу БД для хранения поисковой базы
CREATE TABLE IF NOT EXISTS tablename
(
    emb_labse vector(768),
    ner jsonb,
    question text,
    context text,
    fts tsvector GENERATED ALWAYS AS (to_tsvector('fts_conf'::regconfig, COALESCE(context, ''::text))) STORED
)

где:

  • 'emb_labse' — векторное значение поля context;

  • ner — именованные сущности, найденные в context;

  • question — хранит вопрос датасета;

  • context — содержит единственный релевантный ответ на вопрос (извлекается из датасета);

  • fts — содержит лексический разбор поля context, вычисляется «на лету» при добавлении записи в таблицу

2. Переносим данные из датасета в таблицу БД:
import psycopg2
from psycopg2.extensions import AsIs
import os
import json
import spacy
from langchain_huggingface import HuggingFaceEmbeddings

DB_CONFIG = {} # конфигурация подключения к БД
current_dir = os.path.dirname(__file__)
nlp_model = spacy.load("ru_core_news_lg")

embedding_labse = HuggingFaceEmbeddings(
        model_name="cointegrated/LaBSE-en-ru",
        encode_kwargs={"normalize_embeddings": True},
        model_kwargs={'device': 'cpu'}
    )

def get_ner(text, nlp_model):
    doc = nlp_model(text)
    result = []
    for ent in doc.ents:
        ner = {'ner': ent.lemma_, 'type': ent.label_, 'position': [ent.start_char, ent.end_char]}
        result.append(ner)
    return result

def insert_data(conn, data):
    with conn.cursor() as cursor:
        columns = data.keys()
        values = [data[column] for column in columns]
        insert_statement = 'insert into table_name (%s) values %s'
        cursor.execute(insert_statement, (AsIs(','.join(columns)), tuple(values)))
        conn.commit()

def main():
    conn = psycopg2.connect(**DB_CONFIG)
    with open(os.path.join(current_dir, "dataset_name.json"), "r", encoding="utf-8") as f:
        data = json.load(f)['train']

    for record in data:
         record['emb_labse'] = embedding_labse.embed_query(record['context'])
         record['ner'] = json.dumps(get_ner(record['context'], nlp_model))
         insert_data(conn, record)

if __name__ == "__main__":
    main()

3. Выполняем поиск релевантных документов по списку поисковых запросов.

В качестве эталонного возьмем вопрос датасета: «Сколько островов принадлежит Шотландии?»

Генерируем близкие по смыслу поисковые запросы:

  • «Сколько островов принадлежит Шотландии?»

  • «Сколько островов является частью Шотландии?»

  • «Какое число островов относится к Шотландии?»

  • «Сколько у Шотландии островов?»

  • «Сколько островов есть в Шотландии?»

  • «Какая численность островов, принадлежащих Шотландии?»

  • «Какое количество островов Шотландия имеет в своем составе?»

Пишем код для получения и анализа результатов поиска:
import psycopg2
import os
import spacy
from langchain_huggingface import HuggingFaceEmbeddings
from pgvector.psycopg2 import register_vector
import numpy as np
from collections import defaultdict

DB_CONFIG = {} # конфигурация подключения к базе данных
current_dir = os.path.dirname(__file__)
nlp_model = spacy.load("ru_core_news_lg")

embedding_labse = HuggingFaceEmbeddings(
        model_name="cointegrated/LaBSE-en-ru",
        encode_kwargs={"normalize_embeddings": True},
        model_kwargs={'device': 'cpu'}
    )

def calc_RRF(*list_of_list_ranks_system, K=60):
    rrf_map = defaultdict(float)
    for rank_list in list_of_list_ranks_system:
        for rank, item in enumerate(rank_list, 1):
            rrf_map[item] += 1 / (rank + K)
    sorted_items = sorted(rrf_map.items(), key=lambda x: x[1], reverse=True)
    return sorted_items

def search(conn, emb, query):
    register_vector(conn)
    emb = np.array(emb)    
    sql_query_vector = "SELECT 1 - (emb_labse <-> %s) AS distance, question FROM table_name ORDER BY distance DESC LIMIT 10;"
    query = query.replace(' ', ' or ')
    sql_query_fts_ner = "SELECT ts_rank(fts, query, 8) as rank, question FROM table_name, websearch_to_tsquery('article2_conf', %s) query WHERE query @@ fts and ner @> '[{\"ner\": \"шотландия\"}]' ORDER BY rank DESC;"

    result = {}
    with conn.cursor() as cursor:
        cursor.execute(sql_query_vector, (emb,))
        result_vector = cursor.fetchall()
        for i, _ in enumerate(result_vector, 1):
            if _[1] in result:
                result[_[1]].append(i)
            else:
                result[_[1]] = [i]
                
        cursor.execute(sql_query_fts_ner, (query,))
        result_fts_ner = cursor.fetchall()
        for i, _ in enumerate(result_fts_ner, 1):
            if _[1] in result:
                result[_[1]].append(i)
            else:
                result[_[1]] = ['-', i]
                
        for k,v in result.items():
            if len(v) == 1:
                result[k] = [v[0], '-']

        for i in calc_RRF([_[1] for _ in result_vector], [_[1] for _ in result_fts_ner]):
            result[i[0]].append(i[1])

        result = sorted(result.items(), key=lambda x: x[1][2], reverse=True)[:11]
        print (result[0])
        print ([float(format(_[1][2], '.5f')) for _ in result[1:]])

def main():
    requests = [] # список поисковых запросов
    conn = psycopg2.connect(**DB_CONFIG)
    for query in requests:
        print(f"Query: {query}")
        emb_labse = embedding_labse.embed_query(query)
        search(conn, emb_labse, query)
        print ('-'*40)
    conn.close()

if __name__ == "__main__":
    main()

Анализируем результаты

После выполнения скрипта выше получим следующие данные:

Поисковый запрос

Score с релевантным документом для эталонного запроса

Score c 10 ближайшими документами

Сколько островов принадлежит Шотландии?

0.03278

[0.03077, 0.02996, 0.02944, 0.01613, 0.01613, 0.01587, 0.01562, 0.01562, 0.01515, 0.01493]

Сколько островов является частью Шотландии?

0.03278

[0.02951, 0.02715, 0.01613, 0.01613, 0.01587, 0.01587, 0.01562, 0.01538, 0.01538, 0.01515]

Какое число островов относится к Шотландии?

0.03252

[0.0308, 0.02964, 0.02964, 0.01639, 0.01613, 0.01587, 0.01562, 0.01562, 0.01538, 0.01538]

Сколько у Шотландии островов?

0.03278

[0.02957, 0.0292, 0.02858, 0.01613, 0.01613, 0.01587, 0.01562, 0.01562, 0.01538, 0.01538]

Сколько островов есть в Шотландии?

0.03278

[0.02932, 0.0292, 0.01613, 0.01613, 0.01587, 0.01587, 0.01562, 0.01538, 0.01538, 0.01515]

Какая численность островов, принадлежащих Шотландии? 

0.0327

[0.03101, 0.02878, 0.02859, 0.02844, 0.01613, 0.01613, 0.01587, 0.01587, 0.01562, 0.01538]

Какое количество островов Шотландия имеет в своем составе?

0.03278

[0.0303, 0.02991, 0.02929, 0.0292, 0.01613, 0.01587, 0.01587, 0.01562, 0.01538, 0.01538]

Анализ итогов:

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

  2. Разделение списка документов на релевантные и нерелевантные может быть выполнено автоматически без эмпирического определения нижней границы — массивы score во всех экспериментах четко делятся на два кластера.

Что показывает этот пример

У поиска релевантных документов, основанного только на embedding-моделях, есть ряд ограничений, снижающих качество результата, главные из которых:

а) необходимость эмпирического определения нижней границы релевантности документов;

б) сложность интерпретации результатов поисковой выдачи.

Гибридный поиск — более эффективная альтернатива и имеет следующие преимущества:

а) низкий порог вхождения на этапе проектирования и проверки решения;

б) объяснимость результатов поисковой выдачи;

в) нетребователен к ресурсам при реализации проекта.

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

Комментарии (0)