Публикация

Описание реализации нечёткого поиска дубликатов

Добрый день всем!

В продолжение к теме поиска дублей представляю реализацию одного из алгоритмов нечёткого поиска (спасибо Сергею за идею ;)). Остановлюсь только на описании функциональности. Действия, необходимые для реализации, описаны в инструкции, которая находится в присоединённом архиве вместе с используемыми сервисами и текстами хранимых процедур. Уточню только, что все сервисы из архива не касаются базового функционала, и их можно спокойно загружать. Все изменения, касающиеся базовых скриптов, я описал отдельно.

Итак, допустим, что в нашей системе есть следующие два банка:

Запускаем нечёткий поиск дублей, который находится в меню "Файл"->"Сервис"->"Нечеткий поиск дублей". В появившемся окне устанавливаем значения необходимых нам параметров (их немного: таблица, по которой ищем; поле для поиска; максимальное количество отличий от оригинала):

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

В результате получаем:

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

Ещё один момент: из двух записей основной (которая отображается в верхнем реестре) считается та, значение поля для поиска которой имеет наименьшую длину. Если записи имеют одинаковую длину - они обе отобразятся в верхнем реестре. Продемонстрирую это на двух следующих скриншотах:

В результате:

Думаю, плюсы решения очевидны: а) получаем возможность быстро найти все записи, которые по факту являются дублями, но значения которых не абсолютно равны, а отличаются на несколько символов; б) поиск возможен не только по разделам "Контрагенты"/"Контакты", но также по любой таблице, которая содержит строковые колонки. Особенно это касается справочников.

Теперь о минусах. Первый - очень медленная работа, если в таблице много записей. Что в принципе понятно, поскольку нам необходимо сравнить все возможные пары записей таблицы + для каждой из них посчитать "расстояние" (количество отличий) между ними. И второй - поскольку все необходимые данные получаются с помощью хранимых процедур, которые возвращают свои значения в CustomSQL-колонки сервисов SelectQuery, крайне не рекомендуется включать сортировку по колонкам верхнего реестра, так как произойдёт свал.

Спасибо за внимание и до новых встреч!

Нравится

Поделиться

5 комментариев

Добрый день! почему то когда я выбираю таблицу для поиска выдает в лог такое сообщение ("Невозможно загрузить список для "edtFieldName".Набора данных справочника не определен."). С чем это могло быть связано???

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

Проверьте, пожалуйста, dlFields в окне wnd_FuzzySearch. У него заполнено свойство Dataset? Должно быть mds_FuzzySearchFields.

Попробуйте загрузить сервис из присоединённого архива.

Олег Лабьяк,
разработчик,
3-я линия Службы поддержки Terrasoft.

Спасибо Олег за помощь. Действительно в dlFields не было выбран mds_FuzzySearchFields.

"Лабьяк Олег Игоревич" написал:Теперь о минусах. Первый - очень медленная работа, если в таблице много записей.

При 500 записях в таблице городов поиск очень долго выполняется. Олег, а есть какое-то решение использования обычного поиска дубликатов только для разных таблиц (не только контакты/контрагенты)?

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

Прежде всего, необходимо в окне редактирования wnd_SelectTableFields заменить элемент edtTable типа EnumControl на элемент типа LookupControl с таким же названием. В качестве окна выбора (свойство SelectWindowUSI) указать ему wnd_SelectReportTable, также создать для него отдельный даталинк dlTables, который ссылается на датасет ds_Table.

Далее необходимо внести несколько изменений в скрипт scr_SelectTableFields. Я опишу, как изменить уже существующие функции, но в принципе можно написать свои и вызывать их вместо упомянутых.

Итак, начинаем с функции InitializeTableControl:

function InitializeTableControl(Window) {
	var TableControl = Window.ComponentsByName('edtTable');
	DoPrepareFilters(TableControl);
}

Она вызывает функцию DoPrepareFilters:

function DoPrepareFilters(TablesControl) {
	if (!TablesControl.Value) {
		TablesControl.Value = '{03D2B91C-93A7-4D3F-80DC-8D80C397FBDE}'; // ID таблицы "Контрагенты"
	}
	SelectTableFields.SelectedSubject = GetDatasetFieldValueFromDatasetByUSI('ds_Table',
		'ID', TablesControl.Value, 'Code');
	Connector.Attributes('DuplicateName') = TablesControl.DisplayValue;
	InitializeFields(dlFields.Dataset);
	if (IsDatasetEmpty(dlFields.Dataset)) {
		return;
	}
	InitializeSelectedFields(dlSelectedFields.Dataset, 
		SelectTableFields.DefaultDuplicates); 
	InitializeFilterWindow();
}

Наконец, InitializeFields:

function InitializeFields(Dataset){
	var SubjectDataset = Services.GetNewItemByUSI(SelectTableFields.
		SelectedSubject.replace(/tbl_/, 'ds_'));
	var CanRead = GetCanReadTableGroup(SubjectDataset);
	if (!CanRead) {
		return;
	}
	Dataset.Close();
	Dataset.Open();
	for(var i = 0; i < SubjectDataset.DataFields.Count; i++){
		var DataField = SubjectDataset.DataFields.Items(i);
        AddField(Dataset, DataField);
	}
}

Осталось для контрола edtTable создать обработчик события OnChange. Для предыдущего контрола он был создан, но мы ведь создали новый контрол:

function edtTableOnChange(LookupControl) {
	DoPrepareFilters(LookupControl);
}

Наконец, последний штрих. Открываем скрипт scr_DuplicatesUtils и приводим функцию InitializeDuplicatesProperties к такому виду:

function InitializeDuplicatesProperties(DuplicatesProperties, SelectedSubject, 
	GridArea, Workspace, AndOperation, SelectedDataFields, TestDataset, 
	FieldName, Limit, NotifyObject) {
	DuplicatesProperties.TableName = SelectedSubject;	
	DuplicatesProperties.DatasetName = SelectedSubject.replace(/tbl_/, 'ds_');
	DuplicatesProperties.GridArea = GridArea;
	DuplicatesProperties.Workspace = SelectedSubject.replace(/tbl_/, 'wnd_') + 'sWorkspace';
	DuplicatesProperties.TableCaption = SelectedSubject;	
	DuplicatesProperties.SelectedSubject = SelectedSubject;	
	DuplicatesProperties.AndOperation = AndOperation;		
	DuplicatesProperties.SelectedDataFields = SelectedDataFields;
	DuplicatesProperties.TestDataset = TestDataset;
	DuplicatesProperties.FieldName = FieldName;
	DuplicatesProperties.Limit = Limit;
	DuplicatesProperties.WindowCaption = 
		GetWindowCaptionBySubject(SelectedSubject);
	DuplicatesProperties.SaveButtonCaption = 
		GetSaveButtonCaptionBySubject(SelectedSubject);	
	DuplicatesProperties.NotifyObject = NotifyObject;
	DuplicatesProperties.FieldNames = new Array();
	DuplicatesProperties.DataFieldNames = new Array();
	DuplicatesProperties.DataFieldCaptions = new Array();
	DuplicatesProperties.ResultDatasets = new Array();
}

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

Показать все комментарии