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