Как эффективно искать номенклатуру по части наименования в нескольких полях без запросов в цикле?

Программист 1С v8.3 (Управляемые формы)
← На главную

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

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

Полнотекстовый поиск: наиболее производительное решение

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

Почему ФТП — это хорошо?

  1. Высокая производительность: ФТП работает на уровне платформы и СУБД, используя специализированные индексы. Это обеспечивает гораздо более высокую скорость поиска по сравнению с оператором ПОДОБНО на больших объемах данных.
  2. Гибкость поиска: Поддерживает поиск по нескольким словам, по точной фразе, нечеткий поиск и поиск с учетом синонимов (если настроено). Он способен обрабатывать запросы типа "ААА31 Паста Блендамет" и находить строки "Паста Блендамет ААА31" или "Паста Блендамет ААА31 со вкусом клубники".
  3. Встроенные механизмы: Мы используем готовое, оптимизированное решение платформы, что сокращает трудозатраты на разработку и поддержку.

Как включить и настроить ФТП?

Если ФТП выключен (как в исходной задаче, возможно, "для улучшения производительности"), его включение и настройка являются первым шагом:

  1. Включение: Активируем полнотекстовый поиск в конфигурации и/или в настройках информационной базы.
  2. Индексирование: Выполняем первое (полное) индексирование данных. Это может занять некоторое время, в зависимости от объема информации.
  3. Регламентные задания: Настраиваем регулярные регламентные задания для обновления полнотекстового индекса, чтобы он всегда оставался актуальным. Это важно, так как устаревший индекс не будет отражать последние изменения в данных.

Особенности и потенциальные "подводные камни" ФТП

Хотя ФТП является мощным инструментом, у него есть свои особенности, которые мы должны учитывать:

  1. Обслуживание индекса: Индекс требует регулярного обновления, что может создавать дополнительную нагрузку на систему. Важно правильно настроить расписание регламентных заданий.
  2. Задержка обновления: Обновление индекса может происходить асинхронно, поэтому возможна небольшая задержка между изменением данных и их появлением в результатах поиска.
  3. Точность результатов: В некоторых случаях ФТП может выдавать не всегда адекватные результаты, если требуется очень специфичный поиск или исключение определенных совпадений (например, если "ААА31" — это артикул, а "ААА" — это тип батарейки, ФТП может найти "батарейка ААА", что не всегда желательно). Это особенно актуально, если нужна нечеткая привязка по характеристикам, которые не стандартизированы.
  4. Режим работы: ФТП полноценно функционирует только в клиент-серверном варианте работы 1С.
  5. "Умение готовить": Для эффективного использования ФТП необходимо понимать его принципы работы, настройки и возможности для получения наилучших результатов. Если ФТП был отключен ранее, возможно, из-за неверной настройки, стоит протестировать его на тестовой среде с корректными параметрами.

Использование ФТП в коде обычно сводится к созданию объекта ПолнотекстовыйПоиск, настройке его параметров и вызову метода Найти(). Например:


ПолнотекстовыйПоиск = Новый ПолнотекстовыйПоиск;
ПолнотекстовыйПоиск.ОбластьПоиска.Добавить(Метаданные.РегистрыСведений.ПредложенияПоставщиков); // Или другие объекты
ПолнотекстовыйПоиск.ТекстЗапроса = "ААА31 Паста Блендамет";
ПолнотекстовыйПоиск.МаксимальноеКоличество = 100;

РезультатПоиска = ПолнотекстовыйПоиск.Найти();

Для Каждого ЭлементРезультата Из РезультатПоиска Цикл
    // ЭлементРезультата содержит ОбъектПоиска, Релевантность и др.
    Объект = ЭлементРезультата.ОбъектПоиска;
    Сообщить("Найдено: " + Объект + " (релевантность: " + ЭлементРезультата.Релевантность + ")");
КонецЦикла;

Оптимизированный SQL-запрос с временными таблицами для нечеткого поиска (без ФТП)

Если включение полнотекстового поиска невозможно по каким-либо причинам (политика компании, особенности инфраструктуры или жесткие требования к точности совпадений, несовместимые с ФТП), нам придется реализовать нечеткий поиск с помощью стандартных механизмов запросов 1С, избегая при этом запросов в цикле по строкам табличной части. Мы будем использовать подход с динамическим формированием запроса, временными таблицами и агрегатными функциями.

Принцип работы

Для каждой строки, которую пользователь ввел в табличную часть (например, "ААА31 Паста Блендамет"), мы разбиваем ее на отдельные слова ("ААА31", "Паста", "Блендамет"). Затем мы собираем все эти слова со всех строк табличной части в одну временную таблицу. После этого мы выполняем один большой запрос, который соединяет эту временную таблицу со строками нашего регистра и подсчитывает, сколько слов из поискового запроса содержится в каждой искомой строке регистра. Используя условие ИМЕЮЩИЕ, мы фильтруем результаты, оставляя только те, где найдено нужное количество совпадений (все слова или хотя бы одно).

Шаг 1: Подготовка данных для поиска – формирование временной таблицы слов

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

Предположим, у нас есть табличная часть ТабличнаяЧастьЗапрашиваемаяНоменклатура с колонкой ЗапрашиваемаяНоменклатура (Строка).


// Создаем таблицу значений для хранения всех слов из поисковых строк
ТЗПоисковыеСлова = Новый ТаблицаЗначений;
ТЗПоисковыеСлова.Колонки.Добавить("НомерСтрокиТЧ", Новый ОписаниеТипов("Число")); // Номер исходной строки ТЧ для связи
ТЗПоисковыеСлова.Колонки.Добавить("СловоПоиска", Новый ОписаниеТипов("Строка",,,,Новый КвалификаторыСтроки(50))); // Каждое отдельное слово для поиска

// Обходим табличную часть и разбиваем каждую строку на слова
ИндексСтроки = 0;
Для Каждого СтрокаТЧ Из Объект.ТабличнаяЧастьЗапрашиваемаяНоменклатура Цикл
    ИндексСтроки = ИндексСтроки + 1; // Нумерация строк ТЧ
    
    // Разбиваем строку по пробелам. Игнорируем пустые слова.
    СписокСлов = СтрРазделить(СтрокаТЧ.ЗапрашиваемаяНоменклатура, " ", Ложь); 
    
    Для Каждого Слово Из СписокСлов Цикл
        Если Не ПустаяСтрока(Слово) Тогда
            НоваяСтрока = ТЗПоисковыеСлова.Добавить();
            НоваяСтрока.НомерСтрокиТЧ = ИндексСтроки;
            НоваяСтрока.СловоПоиска = Слово;
        КонецЕсли;
    КонецЦикла;
КонецЦикла;

// Теперь ТЗПоисковыеСлова содержит все слова, сгруппированные по исходным строкам ТЧ.
// Например:
// НомерСтрокиТЧ | СловоПоиска
// -------------|------------
// 1            | ААА31
// 1            | Паста
// 1            | Блендамет
// 2            | Батарейка
// 2            | ААА
// 2            | зайчик

Шаг 2: Формирование и выполнение запроса

Теперь, когда у нас есть таблица со всеми поисковыми словами, мы можем сформировать единый запрос к регистру сведений. Пусть наш регистр сведений называется ПредложенияПоставщиков и имеет следующие поля для поиска: НаименованиеПоставщика (Строка), Артикул (Строка) и НашеНаименование (Ссылка на Справочник.Номенклатура).

Мы будем использовать временную таблицу (ПОМЕСТИТЬ) для наших поисковых слов, а затем соединим ее с основным регистром. Важной особенностью будет использование ЛЕВОГО СОЕДИНЕНИЯ ПО ИСТИНА, которое позволяет соединить каждую строку регистра с каждым поисковым словом. Далее мы используем агрегатные функции и условие ИМЕЮЩИЕ для фильтрации результатов.


Запрос = Новый Запрос;
Запрос.Текст = 
"// Шаг 1: Помещаем подготовленную таблицу слов во временную таблицу запроса
|ВЫБРАТЬ
|    ПоисковыеСлова.НомерСтрокиТЧ КАК НомерСтрокиТЧ,
|    ПоисковыеСлова.СловоПоиска КАК СловоПоиска
|ПОМЕСТИТЬ ПоисковыеСлова
|ИЗ
|    &ТЗПоисковыеСлова КАК ПоисковыеСлова
|;
|
|// Шаг 2: Основной запрос для поиска в регистре
|ВЫБРАТЬ
|    ВложенныйЗапрос.ПредложениеПоставщика КАК ПредложениеПоставщика,
|    ВложенныйЗапрос.Артикул КАК Артикул,
|    ВложенныйЗапрос.НашеНаименование КАК НашеНаименование,
|    ВложенныйЗапрос.НомерСтрокиТЧ КАК НомерСтрокиТЧ,
|    ВложенныйЗапрос.СчетСовпавшихСлов КАК СчетСовпавшихСлов, // Для отладки или сортировки по релевантности
|    ВложенныйЗапрос.ОбщееКоличествоСловДляПоиска КАК ОбщееКоличествоСловДляПоиска
|ИЗ
|    (ВЫБРАТЬ
|        Регистр.ПредложениеПоставщика КАК ПредложениеПоставщика,
|        Регистр.Артикул КАК Артикул,
|        ПРЕДСТАВЛЕНИЕ(Регистр.НашеНаименование) КАК НашеНаименование, // Ищем по представлению ссылки
|        ПоисковыеСлова.НомерСтрокиТЧ КАК НомерСтрокиТЧ,
|        СУММА(ВЫБОР
|            КОГДА (Регистр.ПредложениеПоставщика ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%""
|                ИЛИ Регистр.Артикул ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%""
|                ИЛИ ПРЕДСТАВЛЕНИЕ(Регистр.НашеНаименование) ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%"")
|            ТОГДА 1
|            ИНАЧЕ 0
|        КОНЕЦ) КАК СчетСовпавшихСлов,
|        СУММА(1) КАК ОбщееКоличествоСловДляПоиска // Подсчитываем общее количество слов для текущей строки ТЧ
|    ИЗ
|        РегистрСведений.ПредложенияПоставщиков КАК Регистр 
|            ЛЕВОЕ СОЕДИНЕНИЕ ПоисковыеСлова КАК ПоисковыеСлова
|            ПО ИСТИНА // Соединение каждой строки регистра со всеми словами
|    
|    СГРУППИРОВАТЬ ПО
|        Регистр.ПредложениеПоставщика,
|        Регистр.Артикул,
|        ПРЕДСТАВЛЕНИЕ(Регистр.НашеНаименование),
|        ПоисковыеСлова.НомерСтрокиТЧ
|    ИМЕЮЩИЕ
|        // Условие "И" - все слова из поисковой фразы должны совпасть в одной из колонок
|        СУММА(ВЫБОР
|            КОГДА (Регистр.ПредложениеПоставщика ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%""
|                ИЛИ Регистр.Артикул ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%""
|                ИЛИ ПРЕДСТАВЛЕНИЕ(Регистр.НашеНаименование) ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%"")
|            ТОГДА 1
|            ИНАЧЕ 0
|        КОНЕЦ) = СУММА(1) 
|    ) КАК ВложенныйЗапрос
|
|УПОРЯДОЧИТЬ ПО
|    НомерСтрокиТЧ, СчетСовпавшихСлов УБЫВ
|;
|
|// Если вам нужно условие "ИЛИ" (хотя бы одно слово совпало):
|// Измените секцию ИМЕЮЩИЕ на:
|//    ИМЕЮЩИЕ
|//        СУММА(ВЫБОР
|//            КОГДА (Регистр.ПредложениеПоставщика ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%""
|//                ИЛИ Регистр.Артикул ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%""
|//                ИЛИ ПРЕДСТАВЛЕНИЕ(Регистр.НашеНаименование) ПОДОБНО ""%"" + ПоисковыеСлова.СловоПоиска + ""%"")
|//            ТОГДА 1
|//            ИНАЧЕ 0
|//        КОНЕЦ) > 0 

После формирования текста запроса, устанавливаем параметр и выполняем запрос:


Запрос.УстановитьПараметр("ТЗПоисковыеСлова", ТЗПоисковыеСлова);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();

Важные моменты при использовании этого подхода:

  1. ПРЕДСТАВЛЕНИЕ(Регистр.НашеНаименование): Если поле НашеНаименование является ссылкой на объект (например, на справочник Номенклатура), то для поиска по его текстовому значению (наименованию) необходимо использовать функцию ПРЕДСТАВЛЕНИЕ(). Если же это обычное строковое поле, то функция не нужна.
  2. Чувствительность к регистру: Оператор ПОДОБНО в запросах 1С по умолчанию не чувствителен к регистру символов, что удобно для нечеткого поиска.
  3. Производительность ПОДОБНО: Несмотря на то, что мы избежали цикла, использование множественных условий ПОДОБНО с символом % в начале шаблона может быть неэффективным на очень больших таблицах регистра без специализированных индексов. Однако для строковых полей длиной до 300 символов и разумного объема данных это решение обычно демонстрирует приемлемую производительность.
  4. Группировка по строке неограниченной длины: Если бы поля для поиска были строками неограниченной длины, мы не смогли бы использовать их в секции СГРУППИРОВАТЬ ПО. Однако в ресурсах и измерениях регистров сведений строки обычно имеют ограниченную длину (например, до 300 символов), что позволяет нам группировать по ним.
  5. Логика "И" и "ИЛИ":
    • "И" (все слова найдены): ИМЕЮЩИЕ СчетСовпавшихСлов = ОбщееКоличествоСловДляПоиска. Это означает, что для каждой строки регистра и каждого исходного запроса из ТЧ все слова из этого запроса должны быть найдены в одной из колонок регистра.
    • "ИЛИ" (хотя бы одно слово найдено): ИМЕЮЩИЕ СчетСовпавшихСлов > 0. Это означает, что хотя бы одно слово из поисковой фразы должно быть найдено в одной из колонок регистра.

Преимущества этого подхода

  1. Независимость от ФТП: Решение не требует включения и настройки полнотекстового поиска.
  2. Точный контроль: Мы имеем полный контроль над логикой поиска, можем легко менять условия "И" на "ИЛИ" и определять, по каким именно полям искать.
  3. Один запрос: Мы выполняем всего один запрос к базе данных, даже если в табличной части много строк, что значительно производительнее цикла запросов.

Недостатки

  1. Сложность запроса: Сам текст запроса становится достаточно объемным и сложным для понимания и отладки.
  2. Производительность на очень больших данных: На миллионных объемах данных без правильно настроенных индексов, обход каждой строки с операторами ПОДОБНО %...% может быть медленным, хотя и лучше, чем цикл запросов.

Стандартизация данных и метод N-грамм: долгосрочные улучшения

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

Стандартизация данных

Наиболее элегантным и правильным решением с точки зрения архитектуры является стандартизация хранения данных. Вместо того чтобы пытаться искать "ААА31 Паста Блендамет" в одном текстовом поле, мы можем разбить эту информацию на значимые характеристики и хранить их в отдельных, стандартизированных полях:

  1. Тип: Паста
  2. Марка: Блендамет
  3. Артикул: ААА31
  4. Свойство: со вкусом клубники

В этом случае поиск становится точным и высокопроизводительным: мы ищем "ААА31" в поле "Артикул", "Паста" в поле "Тип" и так далее. Это значительно упрощает запрос и повышает точность результатов.

Преимущества:

  1. Точный поиск: Исключается двусмысленность и неверные совпадения.
  2. Высокая производительность: Поиск по точному значению в индексированных полях работает очень быстро.
  3. Улучшенная структура данных: Система становится более гибкой и масштабируемой.

Недостатки:

  1. Требуется предварительная обработка: Данные из внешних источников необходимо предварительно парсить и стандартизировать перед записью в регистр.
  2. Изменение структуры: Может потребоваться доработка структуры регистра и механизмов загрузки данных.

Метод N-грамм

Метод N-грамм — это продвинутая техника нечеткого поиска, которая разбивает текст на последовательности из N символов (например, биграммы - по 2 символа, триграммы - по 3). Это позволяет находить схожие строки даже при наличии опечаток, пропущенных символов или измененного порядка слов.

Например, для слова "Паста" биграммы будут "Па", "ас", "ст", "та". Если пользователь ввел "Поста", биграммы будут "По", "ос", "ст", "та". Мы видим, что "ст" и "та" совпадают, что указывает на высокую степень сходства.

Преимущества:

  1. Эффективность при опечатках: Отлично подходит для поиска при наличии небольших различий в написании.
  2. Независимость от морфологии: Менее чувствителен к изменениям формы слов.

Недостатки:

  1. Сложность реализации: Требует написания собственного алгоритма или использования внешних компонент, что может быть сложно в 1С.
  2. Увеличение объема данных: Хранение N-грамм может значительно увеличить объем занимаемого дискового пространства.

В контексте 1С без внешних компонент, реализация N-грамм в чистом виде в запросах крайне сложна и неэффективна. Этот метод скорее является основой для создания специализированных поисковых индексов или функций, которые могут быть реализованы на уровне СУБД или внешних сервисов.

Заключение

Мы проанализировали несколько путей решения задачи эффективного нечеткого поиска номенклатуры в 1С. При принятии решения, какой метод выбрать, рекомендуем учитывать следующие факторы:

  1. Возможность использования ФТП: Если полнотекстовый поиск доступен и его включение допустимо, это будет наиболее производительное и гибкое решение. Стоит обсудить с администраторами системы возможность его активации и правильной настройки, предоставив аргументы, что данная задача требует его использования.
  2. Объем данных и требования к производительности: Для небольших и средних объемов данных (десятки тысяч записей в регистре) подход с динамическим запросом и временными таблицами является вполне рабочим и производительным, позволяя избежать запросов в цикле. Для миллионов записей, ФТП будет предпочтительнее.
  3. Долгосрочная перспектива: В идеале, стремимся к стандартизации данных. Это фундаментальное улучшение, которое решает проблему поиска на корню, позволяя выполнять точные запросы по четким характеристикам.

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

← На главную