Re: Нечеткое сравнение слов.
От: Nuzhny Россия https://github.com/Nuzhny007
Дата: 17.10.25 03:26
Оценка: 25 (2) +1
Здравствуйте, vdimas, Вы писали:

V>У меня есть решение, но мне банально интересно, к какой сложности относятся подобные задачи? ))


Сопоставить каждому слову свой вектор в N-мерном пространстве, найти в нем K ближайших за логарифм. И уже с ними линейно сравниться алгоритмом типа Левенштейна.
Найти в пространстве — можно взять готовую библиотеку типа faiss или любой аналог.
Как сопоставить слову вектор — вот тут я бы взял лёгкую нейросеть для текстовых эмбеддингов. Твое требование без ИИ тут не канает, потому что это не ИИ, хоть и нейросеть.
Re: Нечеткое сравнение слов.
От: bnk СССР http://unmanagedvisio.com/
Дата: 17.10.25 09:14
Оценка: 21 (1)
Здравствуйте, vdimas, Вы писали:

V>У меня есть решение, но мне банально интересно, к какой сложности относятся подобные задачи? ))


Клод предлагает 2 основных варианта:

— (рекомендуемый вариант) алгоритм SymSpell как "наиболее популярный и эффективный для этой задачи", O(1)
— BK-дерево (Burkhard-Keller Tree) O(log n)
Отредактировано 17.10.2025 9:18 bnk . Предыдущая версия . Еще …
Отредактировано 17.10.2025 9:17 bnk . Предыдущая версия .
Re: Нечеткое сравнение слов.
От: Janus Россия  
Дата: 18.10.25 06:38
Оценка: 14 (1)
Здравствуйте, vdimas, Вы писали:



V>У меня есть решение, но мне банально интересно, к какой сложности относятся подобные задачи? ))


фонетические алгоритмы здесь?
мы пользовали вот это
... Хорошо уметь читать между строк. Это иногда
приносит большую пользу
Re: Нечеткое сравнение слов.
От: TheBeginner  
Дата: 18.10.25 07:00
Оценка: 14 (1)
Здравствуйте, vdimas, Вы писали:

V>Такая задача.


Посмотрите, может чем поможет: https://flamingo.ics.uci.edu/releases/4.1/
Отредактировано 18.10.2025 7:02 TheBeginner . Предыдущая версия .
Re[2]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 17.10.25 13:05
Оценка: 9 (1)
Здравствуйте, Nuzhny, Вы писали:

N>Как сопоставить слову вектор — вот тут я бы взял лёгкую нейросеть для текстовых эмбеддингов.


Да, вот тут единственно, что плохо понятно в твоём предложении.
Обычно длина "ключевых слов" небольшая (8 символов в среднем), плюс алфавит небольшой — 26 английских букв + 33 русских + 10 цифр плюс некие символы +-*/"(){}, которые все можно закодировать одним символом (раз всё равно потом на найденный список натравливаем Левенштейна), ну и сопоставить английские символы русским и сами русские символы ужать (и-й, е-ё), т.е. можно сделать преобразованный алфавит меньше 32 символов, это 5 бит на символ, т.е. средняя строка влезет в байтовое представление двух int32.

Моё предложенное решение такое:
Построить граф ДКА разбора всех "ключевых" слов. Граф состояний сделать однонаправленным, без циклов, т.е. будет дерево разбора.
Точное соответствие будет найдено за O(N), где N — длина строки.

Для нечёткого соответствия можно протягивать по этому дереву НКА, т.е. множить кол-во вариантов на каждом шаге, с накоплением количества отличий (разницы).
Алгоритм Левенштейна допускает любые опечатки плюс перестановки, т.е. коэфициент размножения кол-ва НКА-состояний на каждом шаге будет 32*2 (сколько символов в алфавите), в конечных состояниях будет список всех слов, которые при преобразовании алфавита дали нам текущую строку разбора — уже сильное ограничение.

При накоплении расстояния/ошибок на каждом шаге большинство НКА будет отваливаться если сделать как принято на практике максимальное расстояние 2. Т.е, грубо, оценка кол-ва протягиваемых НКА на каждом шаге будет 32*32.

Поэтому, есть еще идея допускать не произвольные опечатки, а только самые популярные, например, C-S, X-KS, U-Y, а так же соседние символы на обычной и мобильной клавиатуре (тоже источник опечаток). Т.е. коэф. размножения состояний НКА будет на порядок меньше 10*10.

Из готовых я видел нечеткое сравнение по индексу в библиотеке Lucene (есть джавовская и дотнетная версия), но там такие тормоза, что приплызд.
Поэтому, есть идея искать символ в 3 этапа:
Первый раз прогнать по дереву разбора ДКА-автомат (это даже быстрее, чем поиск по хеш-таблице) — будет найдено или нет точное соответствие.
Затем запуск описанного алгоритма с протягиванием НКА, а затем уже запуск полного нечеткого сравнения из Lucene, если "быстрый" алгоритм не нашёл соответствия.

Если рассуждать дальше, "быстрый" алгоритм можно ограничить, например, одной опечаткой и одной перестановкой, что даст совсем небольшое кол-во одновременно протягиваемых состояний НКА (примерно десяток), т.е. почти мгновенную его работу.

Итого, для полного соответствия будет мгновенный ответ, для 1-й ошибки — почти мгновенный, для более одной ошибки — медленнее всего.
Re[5]: Нечеткое сравнение слов.
От: F3V  
Дата: 20.10.25 18:04
Оценка: 8 (1)
Здравствуйте, vdimas, Вы писали:

F3V>>Уточнённая грамматика


V>Занятный инструмент.

V>Без стёба. ))

Так или иначе получился очень интересный результат:
алгоритм + реализация (ИИ немного посоветовал преобразования):

V>Вот бы нагенерить хотя бы пол-миллиона ключевых слов из алфавита и натравить на разбор десятков различных предложений, где встречаются ключевые слова.


Получился синтетический тест по алфавиту из строчных русских букв.
Запускались тесты для 1000 и 100000 брендов и с 2 внесёнными опечатками.
Генерировались слова по 3,4,5,6,7,8 символов группами с добавлением к старым.
После добавления каждой группы, брались её первые 1000 уникальных слов и проверялись на совпадения.

  Результаты тестов:
===================== ОПЕЧАТОК 2, БРЕНДОВ 5981, ВСЕ ВАРИАНТЫ =========================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|      1000|       983
найденоВариантов    |      1116|      1083|      1095|      1486|      8541|    106542
затрачено           |     24.89|     18.26|     11.99|      3.73|      2.14|      0.98
словВСекунду        |     40.17|     54.78|     83.42|    268.24|    467.29|   1004.09
словВМинуту         |   2410.41|   3286.77|   5005.42|  16094.42|  28037.38|  60245.15
ожидаюТысячуСловЗа  |  00:24.89|  00:18.26|  00:11.99|   00:3.73|   00:2.14|     00:01
===================== ОПЕЧАТОК 2, БРЕНДОВ 5973, ПЕРВЫЙ ВАРИАНТ =======================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|       999|       978
найденоВариантов    |      1000|      1000|      1000|      1000|       999|       978
затрачено           |     13.12|       9.2|      5.91|      1.58|       0.3|      0.02
словВСекунду        |     76.21|    108.73|    169.26|    634.92|   3318.94|  51473.68
словВМинуту         |   4572.47|   6523.87|  10155.72|  38095.24| 199136.21|3088421.05
ожидаюТысячуСловЗа  |  00:13.12|    00:9.2|   00:5.91|   00:1.58|    00:0.3|   00:0.02
===================== ОПЕЧАТОК 2, БРЕНДОВ 524203, ВСЕ ВАРИАНТЫ =======================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|      1000|      1000
найденоВариантов    |      4669|      3133|      3101|     41951|    687313|   3010378
затрачено           |     29.84|     21.55|     13.03|       5.3|      3.49|      2.26
словВСекунду        |     33.51|      46.4|     76.75|    188.79|    286.78|    442.09
словВМинуту         |   2010.66|   2783.71|   4605.11|  11327.17|  17206.77|   26525.2
ожидаюТысячуСловЗа  |  00:29.84|  00:21.55|  00:13.03|    00:5.3|   00:3.49|   00:2.26
===================== ОПЕЧАТОК 2, БРЕНДОВ 524263, ПЕРВЫЙ ВАРИАНТ =====================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|      1000|      1000
найденоВариантов    |      1000|      1000|      1000|      1000|      1000|      1000
затрачено           |      7.04|      6.82|      3.22|      0.37|      0.22|         0
словВСекунду        |    142.13|    146.61|    310.95|   2688.17|   4524.89|    250000
словВМинуту         |   8527.57|   8796.36|  18656.72| 161290.32| 271493.21|  15000000
ожидаюТысячуСловЗа  |   00:7.04|   00:6.82|   00:3.22|   00:0.37|   00:0.22|     00:00
======================================================================================


  Тестовая грамматика производительности.
/*
=== НАСТРОЙКИ ========================================================================
Добавляем в словарь так:
названия  Название-с-символами,samsung, nokia .

Добавляем возможные опечатки так:
опечатки a -> o, u -> a, 0123 -> 4567, аб -> цд .

Можно сделать тест быстродействия так:
использовать алфавит абвгдеёжзийклмнопрстуфхцчшщъыьэюя.
добавить 1000 брендов по 3 символа с 0 ошибок.

=== ФАЙЛ РАЗБОРА =====================================================================
названия "Метро",Домик-в-деревне,samsung,sunsung,nokia,microsoft,1984.

опечатки
abcdefghijklmnopqrstuvwxyz ->
abcdefghijklmnopqrstuvwxyz,
абвгдеёжзийклмнопрстуфхцчшщъыьэюя ->
абвгдеёжзийклмнопрстуфхцчшщъыьэюя,
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ ->
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ,
012345689 -> 012345689.

"Митра" Бобик-в-деревне Домик-в-деревне
sunsung samsung sumsung samsyng sumsyng sumcung
nakia nakua macrosoft microsoft 1945

=== СИНТЕТИЧЕСКИЙ ТЕСТ БЫСТРОДЕЙСТВИЯ ================================================
опечатки
abcdefghijklmnopqrstuvwxyz ->
abcdefghijklmnopqrstuvwxyz,
абвгдеёжзийклмнопрстуфхцчшщъыьэюя ->
абвгдеёжзийклмнопрстуфхцчшщъыьэюя,
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ ->
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ,
012345689 -> 012345689.

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

добавить 100000 брендов по 3 символа.
добавить 100000 брендов по 4 символа.
добавить 100000 брендов по 5 символа.
добавить 100000 брендов по 6 символа.
добавить 100000 брендов по 7 символа.
добавить 100000 брендов по 8 символа.
===================== ОПЕЧАТОК 2, БРЕНДОВ 5981, ВСЕ ВАРИАНТЫ =========================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|      1000|       983
найденоВариантов    |      1116|      1083|      1095|      1486|      8541|    106542
затрачено           |     24.89|     18.26|     11.99|      3.73|      2.14|      0.98
словВСекунду        |     40.17|     54.78|     83.42|    268.24|    467.29|   1004.09
словВМинуту         |   2410.41|   3286.77|   5005.42|  16094.42|  28037.38|  60245.15
ожидаюТысячуСловЗа  |  00:24.89|  00:18.26|  00:11.99|   00:3.73|   00:2.14|     00:01
===================== ОПЕЧАТОК 2, БРЕНДОВ 5973, ПЕРВЫЙ ВАРИАНТ =======================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|       999|       978
найденоВариантов    |      1000|      1000|      1000|      1000|       999|       978
затрачено           |     13.12|       9.2|      5.91|      1.58|       0.3|      0.02
словВСекунду        |     76.21|    108.73|    169.26|    634.92|   3318.94|  51473.68
словВМинуту         |   4572.47|   6523.87|  10155.72|  38095.24| 199136.21|3088421.05
ожидаюТысячуСловЗа  |  00:13.12|    00:9.2|   00:5.91|   00:1.58|    00:0.3|   00:0.02
===================== ОПЕЧАТОК 2, БРЕНДОВ 524203, ВСЕ ВАРИАНТЫ =======================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|      1000|      1000
найденоВариантов    |      4669|      3133|      3101|     41951|    687313|   3010378
затрачено           |     29.84|     21.55|     13.03|       5.3|      3.49|      2.26
словВСекунду        |     33.51|      46.4|     76.75|    188.79|    286.78|    442.09
словВМинуту         |   2010.66|   2783.71|   4605.11|  11327.17|  17206.77|   26525.2
ожидаюТысячуСловЗа  |  00:29.84|  00:21.55|  00:13.03|    00:5.3|   00:3.49|   00:2.26
===================== ОПЕЧАТОК 2, БРЕНДОВ 524263, ПЕРВЫЙ ВАРИАНТ =====================
длинаСлова          |         8|         7|         6|         5|         4|         3
провереноСлов       |      1000|      1000|      1000|      1000|      1000|      1000
найденоВариантов    |      1000|      1000|      1000|      1000|      1000|      1000
затрачено           |      7.04|      6.82|      3.22|      0.37|      0.22|         0
словВСекунду        |    142.13|    146.61|    310.95|   2688.17|   4524.89|    250000
словВМинуту         |   8527.57|   8796.36|  18656.72| 161290.32| 271493.21|  15000000
ожидаюТысячуСловЗа  |   00:7.04|   00:6.82|   00:3.22|   00:0.37|   00:0.22|     00:00
*/
{
  if (!options.бренды) { options.бренды = {}; }
  if (!options.опечатки) { options.опечатки = {}; }
  options.статистика = [];
  options.time = ()=>window.performance.now()
  options.lastTime = 0;
  options.maxTypos = 2;
  options.allVariants = true;
  options.alphabet = 'abcdefghijklmnopqrstuvwxyz';
  options.random =(length) => Math.floor(length * Math.random()) % length;
  options.randomAlpha = ()=> options.alphabet[options.random(options.alphabet.length)];
  options.checkBrands = (бренды, сОпечатками)=>{
    бренды.forEach(_=>{
      var nw = [..._];
      var i = options.random(nw.length);
      var j = options.random(nw.length);
      j = j == i ? (i + 1 < nw.length ? i + 1 : i - 1) : j;
      if (сОпечатками>=1) {
        nw[i] = options.randomAlpha();
      }
      if (сОпечатками>=2) {
        nw[j] = options.randomAlpha();
      }
      for(var i = 0; i < 1; ++i){
        let время3 = options.time();
        let ctx = {варианты:[]};
        if (options.word(ctx,nw,options.maxTypos)) {
          options.addStat(ctx, время3);
        } else {
          console.log(`${_.join('')} != ${nw.join('')}`)
        }
      }
    })
  }
  options.addBrands = (количество, длина, сОпечатками)=>{
   var все = [];
   var строки = {};
   for(var и = 0; и < количество; ++и) {
      var бренд = (()=>{
        var попыток = 0;
        do {
          var слово = [...Array(длина).keys()].map(_=>options.randomAlpha());
        } while(строки[слово] && попыток++ < 10);
        return слово;
      })();
      var строка = бренд.join('');
    options.addBrand(строка);
    if (все.length < 1000 && !строки[строка]) {
      строки[строка] = true;
      все.push(бренд);
    }
   }
   options.checkBrands(все, сОпечатками);
  }
  options.addStat = (ctx,time)=>{
    var время = options.time();
    var затрачено = время - (options.lastTime? options.lastTime: time);
    options.lastTime = время;
    var длинаСлова = ctx.варианты[0].правильно.length;
    var стат = options.статистика[длинаСлова];
    if (!стат) { стат = options.статистика[длинаСлова] =
      {провереноСлов:0,найденоВариантов:0,длинаСлов:0,затрачено:0};}
    стат.длинаСлов += длинаСлова;
    ++стат.провереноСлов;
    стат.найденоВариантов += ctx.варианты.length;
    стат.затрачено += затрачено;
    return ctx;
  }
  options.endStat = (статистика)=>{
    var всё = статистика;
    console.log(статистика)
    всё.forEach(стат=>{
    стат.длинаСлова = стат.длинаСлов / стат.провереноСлов;
    стат.затрачено /= 1000;
    стат.словВСекунду = стат.провереноСлов / стат.затрачено;
    стат.словВМинуту = 60 * стат.словВСекунду;
    стат.ожидаюТысячуСловЗа = options.formatMinutesToTime(options.round(1000 / стат.словВСекунду));
    });
    return статистика;
  }
  options.round = (value)=>{
    if (typeof value == "string"){return value;}
    return Math.round(value*100)/100;
  }
  options.summary = (stat) => {
    const отфильтрованный = stat.filter(Boolean);

    // Сортируем по убыванию длины слова (больше вначале)
    отфильтрованный.sort((a, b) => b.длинаСлова - a.длинаСлова);

    const ключи = [
      'длинаСлова','провереноСлов','найденоВариантов',
      'затрачено','словВСекунду','словВМинуту',
      'ожидаюТысячуСловЗа'
    ];

    // Формируем строки отчёта за один проход
    const строки = ключи.map(поле => {
      const значения = отфильтрованный.map(стат => String(options.round(стат[поле])).padStart(10, ' ')).join('|');
      return поле.padEnd(20, ' ') + '|' + значения;
    });

    строки.unshift(
      `===================== ОПЕЧАТОК ${options.maxTypos}, БРЕНДОВ ${Object.keys(options.бренды).length}, ${options.allVariants?'ВСЕ ВАРИАНТЫ':'ПЕРВЫЙ ВАРИАНТ'} `.
      padEnd(строки[0].length,'='));

    const отчёт = строки.join('\n');
    console.log(отчёт);
    return отчёт;
  }
  options.addHash = (dictionary, text)=>{
    let hash = dictionary[options.hash(text)];
    if (!hash) {
      hash = dictionary[options.hash(text)] = {коллизии:0};
    }
    if (options.коллизии) {
      if (hash.коллизии >= 2) {
        if (!hash.варианты){ hash.варианты = {}; }
        hash.варианты[text.join('')] = true;
      }
    } else {
      ++hash.коллизии;
    }
  }
  options.add = (dictionary, from, to)=>{
      var obj = dictionary;
    [from,to,'.'].forEach(_=>{
      var next = obj[_];
      obj = next = next?next:(obj[_]={});
    });
  }
  options.addBrand = (name)=> options.addHash(options.бренды, name);
  options.addTypo = (from,to)=>
  from.forEach(f=>to.forEach(t=>
    f != t ? options.add(options.опечатки, f,t) : null
  ));
  options.hash = (string) => {
    let hash = 0;
    for (const char of string) {
      hash = (hash << 5) - hash + char.charCodeAt(0);
      hash |= 0; // Constrain to 32bit integer
    }
    return hash;
  };
  options.valid = (ctx,word)=>{
    var коллизии = options.бренды[options.hash(word)];
    if (коллизии && коллизии.коллизии > 1) {
        return !!коллизии.варианты[word.join('')];
    }
    return !!коллизии;
  }
  options.word = (ctx, word, maxTypos) => {
    let успешно = false;

    const addVariant = (typos, nw) => {
      ctx.варианты.push({
        опечаток: typos,
        правильно: nw.join(''),
        оригинал: word.join('')
      });
    };

    if (options.valid(ctx, word)) {
      addVariant(0, word);
      if (maxTypos === 0 || !options.allVariants) return true;
      успешно = true;
    }

    if (maxTypos < 1) return успешно;

    const chars = word;
    const len = chars.length;

    const generateTypos = (positions, typosLeft, nw, start) => {
      if (typosLeft === 0) {
        if (options.valid(ctx, nw)) {
          addVariant(positions.length, nw);
          if (!options.allVariants) return true;
          успешно = true;
        }
        return;
      }

      for (let i = start; i < len; i++) {
        if (positions.includes(i)) continue;
        const список = options.опечатки[chars[i]];
        if (!список) continue;

        for (const оп of Object.keys(список)) {
          const newWord = nw.slice();
          newWord[i] = оп;
          if (generateTypos([...positions, i], typosLeft - 1, newWord, i + 1)){
            return true;
          }
        }
      }
    };

    for (let typosCount = 1; typosCount <= maxTypos && typosCount <= 2; typosCount++) {
      if (generateTypos([], typosCount, chars, 0)) {
        return true;
      }
    }

    return успешно;
  }
  options.formatMinutesToTime = (totalMinutes)=> {
    const hours = Math.floor(totalMinutes / 60);
    const minutes = totalMinutes % 60;
    const formattedHours = String(hours).padStart(2, '0');
    const formattedMinutes = String(minutes).padStart(2, '0');
    return `${formattedHours}:${formattedMinutes}`;
  }
}

words = _ dtime:time description (_ (addBrands/useAlphabet/checkTypos/returnVariants))*
  &{console.log('brands', options.бренды); return true;}
  _ time:time ws:(w:word _ {return w;})*
{
  var времяНастройки = options.formatMinutesToTime((time-dtime) / 1000);
  var времяРазбора = options.formatMinutesToTime((options.time()-time) / 1000);
  return {
    результат:ws,
    времяНастройки,
    времяРазбора,
    статистика:options.summary(options.endStat(options.статистика))};
}

ctx = &(.) {return {варианты:[]};}

skipword = ctx:ctx word:alphabet+
  &{return options.word(ctx,word,options.maxTypos);} { return ctx;}

word = time:time ctx:ctx word:alphabet+
  &{return options.word(ctx,word,options.maxTypos);}
  { return options.addStat(ctx,time);}

_ = [ \t\r\n]*

alphabet = s:[0123456789a-zA-Zа-яА-ЯёЁ\!\-\+\*\"\'] {return s;}
string = s:alphabet+ {return s;}
letter = s:alphabet+ {return s;}

brand = s:string{return s;}

addBrand =
  "названия" _ (s:brand { options.addBrand(s); return null;})
  ("," _ s:brand { options.addBrand(s); return null;})* _ "." {return null;}

addTypo =
  "опечатки" _
  (from:letter _ "->" _ to:letter
    { options.addTypo(from,to); return null;})
  ("," _ from:letter _ "->" _ to:letter
    { options.addTypo(from,to); return null;})* _
  "." {return null}

predescription = desc:((addBrand / addTypo) _)+ {return desc}

description = &predescription
  &{ return options.коллизии = true;}
  desc:predescription {return desc}

time = &{return true;} { return options.time(); }

integer = i:[0123456789]+ {return +i.join('');}
addBrands = "добавить" _ count:integer _ ("брендов"/"бренд") _
  "по" _ length:integer _ ("символа"/"символов") _ "."
  {options.addBrands(count,length,options.maxTypos); return true;}
useAlphabet = "использовать" _ "алфавит"  _ alpha:alphabet+ _ "."
  { options.alphabet = alpha; return alpha;}
checkTypos = "проверять" _ count:integer _
  ("опечаток"/"опечатки"/"опечатку") _ "."
  { options.maxTypos = count; return count;}
returnVariants = "возвращать" _
  all:("все" _ "варианты" {return true;} /
  "первый" _ "вариант" {return false;})
   _ "."
  { options.allVariants = all; return all;}


V>В этой предметной области, смотрю, сравнивают различные алгоритмы/реализации по времени нечеткого поиска 1000 запросов в словаре из полумиллиона ключевых слов.

V>(т.е. десяток примеров надо будет прогнать в цикле 100 раз).

Можно на основе синтетического теста прикинуть.

V>(Если PEG-парсер покажет хоть сколько-нибудь интересные результаты, то можно будет попробовать переписать сгенерированный код на компилируемом языке, например на плюсах, и опять проверить)


Тут сам парсер почти не важен, только код на JavaScript задействован.
PEG-парсер понадобился лишь для удобства и интерактивной настройки и тестирования.

Спасибо за вопрос и за алгоритм.
Re[4]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 20.10.25 00:49
Оценка: 1 (1)
Здравствуйте, Janus, Вы писали:

V>>Допустим, у нас есть алгоритмы поиска расстояния м/у словами.

V>>А как быстро производить нечёткое сравнение в словаре из миллиона слов?
J>Это можно решить на уровне БД

По ссылке:

Как проиндексировать функцию Левенштейна я не придумал

А вопрос был именно в этом. ))

Подсказка:
https://blog.mikemccandless.com/2011/03/lucenes-fuzzyquery-is-100-times-faster.html
https://habr.com/ru/articles/275937/


J>Например pg_trgm в postgres


По ссылке:

поиск по неполному имени с опечатками — 158 мс

Не работает... ))
Re: Нечеткое сравнение слов.
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 17.10.25 19:30
Оценка: :)
Здравствуйте, vdimas, Вы писали:

V>Есть входная строка, например "планшет Samsung"

V>Затем даётся дополнительное ТЗ — необходимо сделать поиск нечётким, т.е. с возможными опечатками.
V>У меня есть решение, но мне банально интересно, к какой сложности относятся подобные задачи? ))

"Гнусмас" или "Порнослоник" опознает?
The God is real, unless declared integer.
Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 17.10.25 01:22
Оценка:
Такая задача.

Есть набор из названий брендов (озвучено — порядка миллиона).
Есть входная строка, например "планшет Samsung"

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

Затем даётся дополнительное ТЗ — необходимо сделать поиск нечётким, т.е. с возможными опечатками.

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

У меня есть решение, но мне банально интересно, к какой сложности относятся подобные задачи? ))
Re[2]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 17.10.25 13:34
Оценка:
Здравствуйте, bnk, Вы писали:

bnk>- (рекомендуемый вариант) алгоритм SymSpell как "наиболее популярный и эффективный для этой задачи", O(1)


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

Но в целом — оч неплохой подход, если много памяти. ))

=====
Единственно что — 2 удаления симметрично не равны расстоянию слов 2, если есть опечатки-вставки до 2-х лишних символов.

Т.е., входные токены надо будет искать последовательно для 2-х удалений, 3-х удалений и 4-х удалений, что уже жопа получается. ))
Тогда придётся ограничить кол-во лишних вставок 1.
Re: Нечеткое сравнение слов.
От: F3V  
Дата: 17.10.25 13:37
Оценка:
Здравствуйте, vdimas, Вы писали:

V>Есть набор из названий брендов (озвучено — порядка миллиона).

V>Есть входная строка, например "планшет Samsung"

V>Необходимо без ИИ сопоставить бренд из словаря этой строке.


V>Затем даётся дополнительное ТЗ — необходимо сделать поиск нечётким, т.е. с возможными опечатками.


Взял PEG.js:

В словаре слово: samsung
Возможные опечатки: u->a | a->u | u->y | y->u
Входные строки вида: samsung samsyng sumsyng

  Грамматика
/*
В словаре слово: samsung
Возможные опечатки: u->a | a->u | u->y | y->u
Входные строки вида: samsung samsyng sumsyng
*/
{
  options.dictionary = {};
  options.typo = {};
  var add = (dictionary, text)=>{
      var obj = dictionary;
    (text+'.').split('').forEach(_=>{
        var next = obj[_];
        obj = next = next?next:(obj[_]={});
    });
  }
  add(options.dictionary, "samsung");
  add(options.typo, "ua");
  add(options.typo, "au");
  add(options.typo, "uy");
  add(options.typo, "yu");
  console.log(options.typo);
  
  options.next = (ctx,sym)=>{
    console.log('next:',JSON.stringify(ctx),sym);
    if (!ctx.tss.length) return false;
    var addts = [];
    ctx.tss = ctx.tss.flatMap(_=>{
        if (!_.ts) { return []; }
        var nnts = {ts:_.ts,res:_.res,orig:_.orig,typos:_.typos};
        _.ts = _.ts[sym];
        _.res = _.ts ? _.res + sym : '';
        _.orig = _.ts ? _.orig + sym : '';
        {
            var tsym = options.typo[sym];
            if (tsym != undefined) {
                var variants = Object.keys(tsym)
                variants.forEach(v=>{
                  var nts = {ts:nnts.ts,res:nnts.res,orig:nnts.orig,typos:nnts.typos};
                  nts.ts = nts.ts[v];
                  nts.orig = nts.ts ? nts.orig + sym : '';
                  nts.res = nts.ts ? nts.res + v : '';
                  ++nts.typos;
                  if (nts.ts) {
                      addts.push(nts);
                  }
                });
            }
        }
        return _.ts ? _ : [];
    }).concat(addts);
    return !!ctx.tss.length;
  }
  
  options.valid = (ctx)=>{
    if (!ctx.tss.length) return false;
    ctx.tss = ctx.tss.map(_=>undefined != _.ts['.']?_:null).filter(_=>_);
    return !!ctx.tss.length;
  }
}

words = (word _)+

ctx = &(.) {return {tss:[{ts:options.dictionary, res:'',orig:'',typos:0}]};}

word = ctx:ctx word:(s:[a-z] &{return options.next(ctx,s);} {return s;})+ &{return options.valid(ctx);} { return ctx;}

_ = [ \t\r\n]*


Нужно подготовить два словаря: брендов и опечаток.
Миллион для таких словарей не много, должно потянуть.

По идее, работает достаточно быстро.
Re[2]: Нечеткое сравнение слов.
От: Hоmunculus  
Дата: 17.10.25 13:51
Оценка:
Здравствуйте, F3V, Вы писали:


F3V>В словаре слово: samsung

F3V>Возможные опечатки: u->a | a->u | u->y | y->u

Почему только эти? Тупо промахнулся на клавиатуре по соседней кнопке (любая буква), или буквы местами перестаивались (у меня часто так) — да полно вариантов опечаток
Re[3]: Нечеткое сравнение слов.
От: F3V  
Дата: 17.10.25 13:57
Оценка:
Здравствуйте, Hоmunculus, Вы писали:

F3V>>Возможные опечатки: u->a | a->u | u->y | y->u


H>Почему только эти? Тупо промахнулся на клавиатуре по соседней кнопке (любая буква), или буквы местами перестаивались (у меня часто так) — да полно вариантов опечаток


Потому что это пруф оф концепт, Minimum Viable Product (минимально жизнеспособный продукт).

Если хотите боевой пример себе сделать, то:

F3V>Нужно подготовить два словаря: брендов и опечаток.
Re: Нечеткое сравнение слов.
От: Pavel Dvorkin Россия  
Дата: 17.10.25 14:34
Оценка:
Здравствуйте, vdimas, Вы писали:

V>Такая задача.


V>Есть набор из названий брендов (озвучено — порядка миллиона).

V>Есть входная строка, например "планшет Samsung"

V>Необходимо без ИИ сопоставить бренд из словаря этой строке.

V>Задача выглядит простой для решения — произвести токенизацию строки и затем поиск в хеш-таблице, где хранятся бренды.

V>Затем даётся дополнительное ТЗ — необходимо сделать поиск нечётким, т.е. с возможными опечатками.


V>И сделать поиск эффективным, т.е. придумать такой алгоритм, который не стал бы последовательно перебирать все слова используя, допустим, стороннюю библиотеку поиска дистанции между словами (есть полно готовых таких алгоритмов).


V>У меня есть решение, но мне банально интересно, к какой сложности относятся подобные задачи? ))


Во-первых, отмечу, что решение точным быть не может. "Победа" и "Обеда" могут быть всегда признаны одинаковыми. А может, и "Беда" сюда же.

Далее в предположении, что имена брендов содержат только латинские буквы. Если это не так — нужно больше битов.

Сопоставляем каждому имени бренда 32-битное число BB, в котором каждый бит указывает на наличие соответствующей буквы алфавита (бит0 — есть буква a, бит1 — есть буква b и т.д.). Если в имени бренда некоторая буква встречается >1 раза, просто игнорируем этот факт. Это можно сделать заранее. А также для каждого имени бренда определяем длину слова BL

Для кандидата выполняем те же действия, получив аналогичное число CB и длину CL

Если разница длин BL и CL больше некоторого порога PL — отвергаем кандидата. Слишком большое различие длин.

int D = CB & BB;

Если в D единичных битов меньше некоторого порога (зависящего от длины слова, тут надо подумать), имя B[i] отвергается. Слишком большое различие набора букв.

Для оставшихся B[i] проводим сравнение всерьез — по Левенштейну, скажем.
With best regards
Pavel Dvorkin
Отредактировано 17.10.2025 14:42 Pavel Dvorkin . Предыдущая версия . Еще …
Отредактировано 17.10.2025 14:35 Pavel Dvorkin . Предыдущая версия .
Re[2]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 17.10.25 19:36
Оценка:
Здравствуйте, F3V, Вы писали:

F3V>Нужно подготовить два словаря: брендов и опечаток.


На слово-название бренда длиной 8 символов получается более 350 тыс вариантов с опечатками до 2 шт.


F3V>Миллион для таких словарей не много, должно потянуть.


Из миллиона слов получится 350 миллиардов вариантов.
Пусть даже часть опечаток от разных ключевых слов склеятся в один, это не спасёт. ))

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

Т.е., алгоритм такой:
— проверили точное соответствие;
— если не нашли, то нагенерили все варианты с одной опечаткой (это замена одной буквы=N*K, где K — размер алфавита, плюс все соседние перестановки=N, плюс все удаления по одному символу=N, т.е. разветвление получается примерно N(K+2) на каждую ошибюку), проверили в цикле все нагенерённые варианты;
— если не нашли, нагенерили из каждого варианта еще варианты, в этом месте и получается более 350 тыс уже при допущении 2-х ошибок.

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

Поэтому, в таких системах живёт магическое число 2.

Однако, в реальных системах проверки правописания всё чуть проще, т.к. алфавит намного уже. А названия брендов могут состоять, к примеру, из русских и латинских букв, плюс цифры, плюс различные знаки !-+* и т.д. Т.е. алфавит в разы больше, разветвление на каждой ошибке слишком большое.
Re[2]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 17.10.25 19:57
Оценка:
Здравствуйте, Pavel Dvorkin, Вы писали:

PD>Сопоставляем каждому имени бренда 32-битное число BB, в котором каждый бит указывает на наличие соответствующей буквы алфавита (бит0 — есть буква a, бит1 — есть буква b и т.д.). Если в имени бренда некоторая буква встречается >1 раза, просто игнорируем этот факт. Это можно сделать заранее. А также для каждого имени бренда определяем длину слова BL

PD>Для кандидата выполняем те же действия, получив аналогичное число CB и длину CL

Я тоже думал о различных вариантах приближённого кодирования, но оно не давало ничего в терминах сложности O.

Более того, перекодированные варианты неудобны при нахождении ближайших.

Например, у обычных чисел мы можем найти ближайших соседей двоичным поиском в отсортированном массиве.
А если все биты равноправны, что тогда делать? ))

Есть вариант автомата Ливенштейна.
Я тоже за считанные минуты пришёл к похожему решению.
В терминах сложности получается похожее решение, но у него есть предварительное кодирование.
https://habr.com/ru/articles/275937/

Реализация существует на Джава и C#, реализация ужасна — я бы пристрелил. ))

Но там это объяснимо, т.к. эта система парсит и строит автоматы на лету, т.е. на вход подаются выражения навроде "find Samsong~" (специально написал с опечаткой).
Тильда означает нечеткий поиск в индексе.

А так-то для конкретной выделенной задачи можно сделать всё плоско в памяти, резко её сэкономив и улучшив быстродействие в разы.
Re[3]: Нечеткое сравнение слов.
От: pik Италия  
Дата: 17.10.25 20:23
Оценка:
Здравствуйте, vdimas, Вы писали:



F3V>>Нужно подготовить два словаря: брендов и опечаток.


V>На слово-название бренда длиной 8 символов получается более 350 тыс вариантов с опечатками до 2 шт.



наиболее реально составить список наиболее вероятных опечаток и прогонять их через regex.
Re[4]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 17.10.25 21:06
Оценка:
Здравствуйте, pik, Вы писали:

pik>наиболее реально составить список наиболее вероятных опечаток и прогонять их через regex.


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

И не заранее составить список всех самых вероятных опечаток (там всё равно получается приличный комбинаторный взрыв вариантов, память слишком отожрет), а прогонять одновременно варианты с дистанцией до 2-х ошибок, т.е. выстраивать эти варианты динамически. Ну или же перебирать эти варианты у исходного слова и сравнивать со словарём все эти варианты поочерёдно — суть та же, но другая реализация. Тут только сравнительное тестирование покажет. ))

Вот тут раскрыл примерную идею:
https://www.rsdn.org/forum/alg/9006963.1
Отредактировано 17.10.2025 21:13 vdimas . Предыдущая версия .
Re[3]: Нечеткое сравнение слов.
От: bnk СССР http://unmanagedvisio.com/
Дата: 18.10.25 07:49
Оценка:
Здравствуйте, vdimas, Вы писали:

V>Здравствуйте, bnk, Вы писали:


bnk>>- (рекомендуемый вариант) алгоритм SymSpell как "наиболее популярный и эффективный для этой задачи", O(1)


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

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

Насколько я понял там формально доказано что удаления эквивалентны опечаткам, поэтому это и работает
Re[3]: Нечеткое сравнение слов.
От: F3V  
Дата: 18.10.25 09:49
Оценка:
Здравствуйте, vdimas, Вы писали:

V>Из миллиона слов получится 350 миллиардов вариантов.

V>Пусть даже часть опечаток от разных ключевых слов склеятся в один, это не спасёт. ))

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

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

V>Некоторые алгоритмы предлагают подход от обратного — генерить всевозможные опечатки не словаря, а входного токена.


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

V>Т.е., алгоритм такой:

V>- проверили точное соответствие;
V>- если не нашли, то нагенерили все варианты с одной опечаткой (это замена одной буквы=N*K, где K — размер алфавита, плюс все соседние перестановки=N, плюс все удаления по одному символу=N, т.е. разветвление получается примерно N(K+2) на каждую ошибюку), проверили в цикле все нагенерённые варианты;
V>- если не нашли, нагенерили из каждого варианта еще варианты, в этом месте и получается более 350 тыс уже при допущении 2-х ошибок.

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

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

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


В языке есть морфологические правила. Но с брендами такое вряд ли пройдёт.

V>Однако, в реальных системах проверки правописания всё чуть проще, т.к. алфавит намного уже. А названия брендов могут состоять, к примеру, из русских и латинских букв, плюс цифры, плюс различные знаки !-+* и т.д. Т.е. алфавит в разы больше, разветвление на каждой ошибке слишком большое.


  Уточнённая грамматика.
/*
=== НАСТРОЙКИ ===
Добавляем в словарь так:
названии  Название-с-символами,samsung, nokia .

Добавляем возможные опечатки так:
опечатки a -> o, u -> a, 0123 -> 4567, аб -> цд .

=== ФАЙЛ РАЗБОРА ===
названия "Метро",Домик-в-деревне,samsung,sunsung,nokia,microsoft,1984.

опечатки
abcdefghijklmnopqrstuvwxyz ->
abcdefghijklmnopqrstuvwxyz,
абвгдеёжзийклмнопрстуфхцчшщъыьэюя ->
абвгдеёжзийклмнопрстуфхцчшщъыьэюя,
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ ->
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ,
012345689 -> 012345689.

"Митра" Бобик-в-деревне Домик-в-деревне
sunsung samsung sumsung samsyng sumsyng sumcung
nakia nakua macrosoft microsoft 1945
*/
{
  if (!options.бренды) { options.бренды = {}; }
  if (!options.опечатки) { options.опечатки = {}; }
  options.addHash = (dictionary, text)=>{
    let hash = dictionary[options.hash(text)];
    if (!hash) {
      hash = dictionary[options.hash(text)] = {коллизии:0};
    }
    if (options.коллизии) {
      if (hash.коллизии >= 2) {
        if (!hash.варианты){ hash.варианты = {}; }
        hash.варианты[text.join('')] = true;
      }
    } else {
      ++hash.коллизии;
    }
  }
  options.add = (dictionary, from, to)=>{
      var obj = dictionary;
    [from,to,'.'].forEach(_=>{
      var next = obj[_];
      obj = next = next?next:(obj[_]={});
    });
  }
  options.addBrand = (name)=> options.addHash(options.бренды, name);
  options.addTypo = (from,to)=>
  from.forEach(f=>to.forEach(t=>
    f != t ? options.add(options.опечатки, f,t) : null
  ));
  options.hash = (string) => {
    let hash = 0;
    for (const char of string) {
      hash = (hash << 5) - hash + char.charCodeAt(0);
      hash |= 0; // Constrain to 32bit integer
    }
    return hash;
  };
  options.valid = (ctx,word)=>{
    var коллизии = options.бренды[options.hash(word)];
    if (коллизии && коллизии.коллизии > 1) {
        return !!коллизии.варианты[word.join('')];
    }
    return !!коллизии;
  }
  options.word = (ctx,word,maxTypos)=>{
    if (options.valid(ctx,word)){
      ctx.варианты.push({
        опечаток: 0,
        правильно: word.join(''),
        оригинал: word.join('')
      });
      if (maxTypos == 0) {
        return true;
      }
    }
    if (maxTypos >= 1) {
      word.forEach((_,i)=>{
        var список = options.опечатки[_];
        if (!список){return;}
        var опечатки = Object.keys(список);
        опечатки.forEach(оп=>{
          var nw = [...word];
          nw[i] = оп;
          if (options.valid(ctx,nw)){
            ctx.варианты.push({
              опечаток: 1,
              правильно: nw.join(''),
              оригинал: word.join('')
            });
          }
        });
      });
    }
    if (maxTypos >= 2) {
      word.forEach((_,i)=>{
        var список = options.опечатки[_];
        if (!список){return;}
        var опечатки = Object.keys(список);
        word.forEach((__,j)=>{
          if (i<=j){return;}
          var списокJ = options.опечатки[__];
          if (!списокJ){return;}
          var опечаткиJ = Object.keys(списокJ);
          опечатки.forEach(оп=>{
            опечаткиJ.forEach(опJ=>{
              var nw = [...word];
              nw[i] = оп;
              nw[j] = опJ;
              if (options.valid(ctx,nw)) {
                ctx.варианты.push({
                  опечаток: 2,
                  правильно: nw.join(''),
                  оригинал: word.join('')
                });
              }
            });
          });
        });
      });
    }
    return !!ctx.варианты.length;
  }
  options.formatMinutesToTime = (totalMinutes)=> {
    const hours = Math.floor(totalMinutes / 60);
    const minutes = totalMinutes % 60;
    const formattedHours = String(hours).padStart(2, '0');
    const formattedMinutes = String(minutes).padStart(2, '0');
    return `${formattedHours}:${formattedMinutes}`;
  }
}

words = dtime:time description
  &{console.log('brends', options.бренды); return true;}
  _ time:time ws:(w:word _ {return w;})+
{
  var времяНастройки = options.formatMinutesToTime((time-dtime) / 1000);
  var времяРазбора = options.formatMinutesToTime((Date.now()-time) / 1000);
  return {результат:ws, времяНастройки, времяРазбора};
}

ctx = &(.) {return {варианты:[]};}

word = ctx:ctx word:alphabet+ &{return options.word(ctx,word,3);} { return ctx;}

_ = [ \t\r\n]*

alphabet = s:[0123456789a-zA-Zа-яА-ЯёЁ\!\-\+\*\"\'] {return s;}
string = s:alphabet+ {return s;}
letter = s:alphabet+ {return s;}

brand = s:string{return s;}

addBrand =
  "названия" _ (s:brand { options.addBrand(s); return null;})
  ("," _ s:brand { options.addBrand(s); return null;})* _ "." {return null;}

addTypo =
  "опечатки" _ (from:letter _ "->" _ to:letter { options.addTypo(from,to); return null;})
  ("," _ from:letter _ "->" _ to:letter { options.addTypo(from,to); return null;})* _ "." {return null}

predescription = desc:((addBrand / addTypo) _)+ {return desc}

description = &predescription &{ return options.коллизии = true;} desc:((addBrand / addTypo) _)+ {return desc}

time = &(.) { return Date.now(); }


Расширил алфавит русскими буквами, цифрами, знаками !-+*.

При коллизии хешей, сравнение дополнительно делается по содержимому строки.

Добавил чтение конфигурации парсера непосредственно из файла разбора:

названия "Метро",Домик-в-деревне,samsung,sunsung,nokia,microsoft,1984.

опечатки
abcdefghijklmnopqrstuvwxyz ->
abcdefghijklmnopqrstuvwxyz,
абвгдеёжзийклмнопрстуфхцчшщъыьэюя ->
абвгдеёжзийклмнопрстуфхцчшщъыьэюя,
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ ->
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ,
012345689 -> 012345689.

"Митра" Бобик-в-деревне Домик-в-деревне
sunsung samsung sumsung samsyng sumsyng sumcung
nakia nakua macrosoft microsoft 1945


  Результат разбора
{
  'результат': [
    {
      'варианты': [
        {
          'опечаток': 2,
          'правильно': '"Метро"',
          'оригинал': '"Митра"'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 2,
          'правильно': 'Домик-в-деревне',
          'оригинал': 'Бобик-в-деревне'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 0,
          'правильно': 'Домик-в-деревне',
          'оригинал': 'Домик-в-деревне'
        },
        {
          'опечаток': 2,
          'правильно': 'Домик-в-дердёне',
          'оригинал': 'Домик-в-деревне'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 0,
          'правильно': 'sunsung',
          'оригинал': 'sunsung'
        },
        {
          'опечаток': 2,
          'правильно': 'samsung',
          'оригинал': 'sunsung'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 0,
          'правильно': 'samsung',
          'оригинал': 'samsung'
        },
        {
          'опечаток': 2,
          'правильно': 'sunsung',
          'оригинал': 'samsung'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 1,
          'правильно': 'samsung',
          'оригинал': 'sumsung'
        },
        {
          'опечаток': 1,
          'правильно': 'sunsung',
          'оригинал': 'sumsung'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 1,
          'правильно': 'samsung',
          'оригинал': 'samsyng'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 2,
          'правильно': 'samsung',
          'оригинал': 'sumsyng'
        },
        {
          'опечаток': 2,
          'правильно': 'sunsung',
          'оригинал': 'sumsyng'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 2,
          'правильно': 'samsung',
          'оригинал': 'sumcung'
        },
        {
          'опечаток': 2,
          'правильно': 'sunsung',
          'оригинал': 'sumcung'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 1,
          'правильно': 'nokia',
          'оригинал': 'nakia'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 2,
          'правильно': 'nokia',
          'оригинал': 'nakua'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 1,
          'правильно': 'microsoft',
          'оригинал': 'macrosoft'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 0,
          'правильно': 'microsoft',
          'оригинал': 'microsoft'
        }
      ]
    },
    {
      'варианты': [
        {
          'опечаток': 2,
          'правильно': '1984',
          'оригинал': '1945'
        }
      ]
    }
  ],
  'времяНастройки': '00:0.007',
  'времяРазбора': '00:0.205'
}


Интересно, что даже на тестовом наборе взятая hash-функция привела к коллизии:
    {
      'варианты': [
        {
          'опечаток': 0,
          'правильно': 'Домик-в-деревне',
          'оригинал': 'Домик-в-деревне'
        },
        {
          'опечаток': 2,
          'правильно': 'Домик-в-дердёне',
          'оригинал': 'Домик-в-деревне'
        }
      ]
    },


Т.е. при добавление брендов коллизии уточняются, но если в разбираемом контенте есть бренд с коллизией, то её уточнить уже не получается.
Re[4]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 18.10.25 14:11
Оценка:
Здравствуйте, F3V, Вы писали:

F3V>По идее, количество вероятных ошибок должно зависеть от длины слова, но пусть будет пока две.


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


F3V>Уточнённая грамматика


Занятный инструмент.
Без стёба. ))

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

В этой предметной области, смотрю, сравнивают различные алгоритмы/реализации по времени нечеткого поиска 1000 запросов в словаре из полумиллиона ключевых слов.
(т.е. десяток примеров надо будет прогнать в цикле 100 раз).

https://medium.com/data-science/symspell-vs-bk-tree-100x-faster-fuzzy-string-search-spell-checking-c4f10d80a078

(Если PEG-парсер покажет хоть сколько-нибудь интересные результаты, то можно будет попробовать переписать сгенерированный код на компилируемом языке, например на плюсах, и опять проверить)
Re[4]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 18.10.25 14:19
Оценка:
Здравствуйте, bnk, Вы писали:

bnk>Насколько я понял там формально доказано что удаления эквивалентны опечаткам, поэтому это и работает


Удаления эквивалентны опечаткам-заменам, разумеется.

Но для опечаток-перестановок (поменяли соседние символы местами) расстояние между словами считается 1, но SymSpell для этого случая требует 2 удаления, т.е. рассматривает этот вид опечаток как расстояние 2. ))

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

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

В целом подход оччень неплох...
Отредактировано 18.10.2025 14:20 vdimas . Предыдущая версия . Еще …
Отредактировано 18.10.2025 14:20 vdimas . Предыдущая версия .
Re[2]: Нечеткое сравнение слов.
От: vdimas Россия  
Дата: 18.10.25 14:38
Оценка:
Здравствуйте, Janus, Вы писали:

J>фонетические алгоритмы здесь?


Ага — это первое что приходит в голову (и тут тоже многим сходу пришло) — это использовать такое св-во, как "наиболее типичные ошибки".
Фонетические алгоритмы как раз из этой области — производят подстановки "похожих" символов и их сочетаний.
Целью является уменьшение алфавита, ес-но.


J>мы пользовали вот это


Допустим, у нас есть алгоритмы поиска расстояния м/у словами.
А как быстро производить нечёткое сравнение в словаре из миллиона слов?
Re[3]: Нечеткое сравнение слов.
От: Janus Россия  
Дата: 19.10.25 10:37
Оценка:
Здравствуйте, vdimas, Вы писали:



V>Допустим, у нас есть алгоритмы поиска расстояния м/у словами.

V>А как быстро производить нечёткое сравнение в словаре из миллиона слов?

Это можно решить на уровне БД

Например pg_trgm в postgres
... Хорошо уметь читать между строк. Это иногда
приносит большую пользу
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.