Нечеткий поиск JavaScript

Я работаю над этой фильтрацией, где у меня есть около 50-100 элементов списка. И каждый элемент имеет разметку следующим образом:

<li>
  <input type="checkbox" name="services[]" value="service_id" />
  <span class="name">Restaurant in NY</span>
  <span class="filters"><!-- hidden area -->
    <span class="city">@city: new york</span>
    <span class="region">@reg: ny</span>
    <span class="date">@start: 02/05/2012</span>
    <span class="price">@price: 100</span>
  </span>
</li>

Я создал такую ​​разметку, потому что изначально использовал List.js.

Итак, вы, наверное, уже догадались, что я хочу делать такие поиски: @region: LA @price: 124 и так далее. Проблема в том, что я также хочу отобразить более одного элемента, чтобы выбрать более... одного :)

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

Любая идея или отправная точка?

// редактировать: поскольку у меня довольно небольшое количество элементов, я хотел бы решение на стороне клиента.


person Ionuț Staicu    schedule 09.02.2012    source источник
comment
Проверьте это: code.google.com/p/yeti-witch — может быть полезным.   -  person techfoobar    schedule 09.02.2012
comment
Также посмотрите, позволяет ли ваше требование переместить часть нечеткого поиска на сторону сервера (с помощью AJAX). Если да, то сделать это с помощью solr будет проще всего. В дополнение к тому, что вы можете искать среди тысяч предметов в кратчайшие сроки. lucene.apache.org/solr   -  person techfoobar    schedule 09.02.2012
comment
Techfoobar: спасибо, но Yeti больше похож на java, чем на javascript. Я не могу понять, как использовать его в моем существующем коде. Кроме того, solr, похоже, тоже java. Мне нужно что-то на стороне клиента или PHP.   -  person Ionuț Staicu    schedule 09.02.2012
comment
Я не вижу, где вы видите нечеткий поиск. Вы не упомянули ничего, что заставило бы меня подумать, что вам нужен нечеткий поиск. Или я что-то упускаю? Нечеткий поиск использует нечеткие категории, которые не имеют строго определенных границ. В вашем случае я вижу строгий поиск, который будет соответствовать более чем одному свойству.   -  person Matjaz Muhic    schedule 09.02.2012
comment
@Matjaz, я не был уверен, как это называется. Это только то, что я предполагал :) Спасибо за разъяснение, надеюсь, я смогу провести более целенаправленный поиск.   -  person Ionuț Staicu    schedule 09.02.2012
comment
Эй, нет проблем. Но я частично спал и не знал, то ли я не знал, что вы имеете в виду, или вы просто перепутали это с чем-то .. :)   -  person Matjaz Muhic    schedule 09.02.2012


Ответы (7)


Я искал «нечеткий поиск» в javascript, но не нашел здесь решения, поэтому я написал свою собственную функцию, которая делает то, что мне нужно.

Алгоритм очень прост: прокрутите буквы-иголки и проверьте, встречаются ли они в том же порядке в стоге сена:

String.prototype.fuzzy = function (s) {
    var hay = this.toLowerCase(), i = 0, n = -1, l;
    s = s.toLowerCase();
    for (; l = s[i++] ;) if (!~(n = hay.indexOf(l, n + 1))) return false;
    return true;
};

e.g.:

('a haystack with a needle').fuzzy('hay sucks');    // false
('a haystack with a needle').fuzzy('sack hand');    // true
person Dziad Borowy    schedule 06.03.2013
comment
Это аккуратно (за исключением манипулирования строковым прототипом) и подходит для некоторых вариантов использования, но более сложный нечеткий поиск также должен возвращать наиболее релевантные результаты. Я предполагаю, что это будет основано на количестве символов, которые являются смежными. - person Ian Clark; 17.06.2018
comment
Это так здорово. Я использую это почти во всем, что строю. Спасибо, что поделились @tborychowski! - person RuNpiXelruN; 25.01.2019

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

person Ionuț Staicu    schedule 30.05.2013
comment
он выполняет поиск, но на самом деле это не нечеткий поиск. например в их демонстрационном нечетком запросе: bruwo не находит Гайбраша Трипвуда... - person Dziad Borowy; 03.06.2013
comment
Вы можете указать, насколько нечетким должен быть поиск. Вы можете ввести brew, и Guybrush Treepwood действительно отобразится. Когда вы получаете два символа из второго слова, результат отфильтровывается. - person Matt Sgarlata; 05.06.2013

Другое (простое) решение. Не чувствителен к регистру и игнорирует порядок букв.

Он выполняет проверку для каждой буквы поискового термина. Если исходная строка содержит эту букву, она будет считаться вверх (или вниз, если нет). В зависимости от соотношения совпадений и длины строки он вернет true или false.

String.prototype.fuzzy = function(term, ratio) {
    var string = this.toLowerCase();
    var compare = term.toLowerCase();
    var matches = 0;
    if (string.indexOf(compare) > -1) return true; // covers basic partial matches
    for (var i = 0; i < compare.length; i++) {
        string.indexOf(compare[i]) > -1 ? matches += 1 : matches -=1;
    }
    return (matches/this.length >= ratio || term == "")
};

Примеры:

("Test").fuzzy("st", 0.5) // returns true
("Test").fuzzy("tes", 0.8) // returns false cause ratio is too low (0.75)
("Test").fuzzy("stet", 1) // returns true
("Test").fuzzy("zzzzzest", 0.75) // returns false cause too many alien characters ("z")
("Test").fuzzy("es", 1) // returns true cause partial match (despite ratio being only 0.5)
person gnj    schedule 06.10.2016

Меня не устраивал list.js, поэтому я создал свой собственный. Вероятно, это не совсем нечеткий поиск, но я не знаю, как его назвать. Я просто хотел, чтобы он соответствовал запросу, независимо от порядка моих слов в запросе.

Рассмотрим следующий сценарий:

  • сборник статей в памяти существует
  • порядок появления слов запроса не имеет значения (например, «привет мир» против «мир привет»)
  • Код должен быть легко читаемым

Вот пример:

var articles = [{
  title: '2014 Javascript MVC Frameworks Comparison',
  author: 'Guybrush Treepwood'
}, {
  title: 'Javascript in the year 2014',
  author: 'Herman Toothrot'
},
{
  title: 'Javascript in the year 2013',
  author: 'Rapp Scallion'
}];

var fuzzy = function(items, key) {
  // Returns a method that you can use to create your own reusable fuzzy search.

  return function(query) {
    var words  = query.toLowerCase().split(' ');

    return items.filter(function(item) {
      var normalizedTerm = item[key].toLowerCase();

      return words.every(function(word) {
        return (normalizedTerm.indexOf(word) > -1);
      });
    });
  };
};


var searchByTitle = fuzzy(articles, 'title');

searchByTitle('javascript 2014') // returns the 1st and 2nd items

Ну, я надеюсь, что это поможет кому-то там.

person Marc Lundgren    schedule 10.12.2014

У меня есть небольшая функция, ищущая строку в массиве (по крайней мере, для меня она дает лучшие результаты, чем levenshtein):

function fuzzy(item,arr) {
  function oc(a) {
    var o = {}; for (var i=0; i<a.length; i++) o[a[i]] = ""; return o;
  }
  var test = [];
  for (var n=1; n<=item.length; n++)
    test.push(item.substr(0,n) + "*" + item.substr(n+1,item.length-n));
  var result = [];
  for (var r=0; r<test.length; r++) for (var i=0; i<arr.length; i++) {
    if (arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[0]) != -1)
    if (arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[1]) != -1)
    if (0 < arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[1]) 
          - arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*")[0] < 2 ) )
    if (!(arr[i] in oc(result)))  result.push(arr[i]);
  }
  return result;
}
person Roland Hentschel    schedule 28.10.2012
comment
Почему ты звонишь arr[i].toLowerCase().indexOf(test[r].toLowerCase().split("*") снова и снова?! - person Metalstorm; 28.07.2015

И я сделал свой собственный. Он использует regex и служит скорее доказательством -концепция, так как она полностью не стресс-тестирована.

наслаждайтесь нечетким поиском/нечетким соответствием JavaScript http://unamatasanatarai.github.io/FuzzyMatch/test/index.html

person Unamata Sanatarai    schedule 13.04.2014

Представленные здесь решения возвращают true/false и не содержат информации о том, какая часть совпала, а какая нет.

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

Я создал собственное решение на машинописном языке (если вы хотите его использовать — я опубликовал его здесь — https://github.com/pie6k/fuzzystring) и демо здесь https://pie6k.github.io/fuzzystring/

Это работает так:

fuzzyString('liolor', 'lorem ipsum dolor sit');

// returns
{
  parts: [
    { content: 'l', type: 'input' },
    { content: 'orem ', type: 'fuzzy' },
    { content: 'i', type: 'input' },
    { content: 'psum d', type: 'fuzzy' },
    { content: 'olor', type: 'input' },
    { content: ' sit', type: 'suggestion' },
  ],
  score: 0.87,
}

Вот полная реализация (Typescript)

type MatchRoleType = 'input' | 'fuzzy' | 'suggestion';

interface FuzzyMatchPart {
  content: string;
  type: MatchRoleType;
}

interface FuzzyMatchData {
  parts: FuzzyMatchPart[];
  score: number;
}

interface FuzzyMatchOptions {
  truncateTooLongInput?: boolean;
  isCaseSesitive?: boolean;
}

function calculateFuzzyMatchPartsScore(fuzzyMatchParts: FuzzyMatchPart[]) {
  const getRoleLength = (role: MatchRoleType) =>
    fuzzyMatchParts
      .filter((part) => part.type === role)
      .map((part) => part.content)
      .join('').length;

  const fullLength = fuzzyMatchParts.map((part) => part.content).join('')
    .length;
  const fuzzyLength = getRoleLength('fuzzy');
  const inputLength = getRoleLength('input');
  const suggestionLength = getRoleLength('suggestion');

  return (
    (inputLength + fuzzyLength * 0.7 + suggestionLength * 0.9) / fullLength
  );
}

function compareLetters(a: string, b: string, isCaseSensitive = false) {
  if (isCaseSensitive) {
    return a === b;
  }
  return a.toLowerCase() === b.toLowerCase();
}

function fuzzyString(
  input: string,
  stringToBeFound: string,
  { truncateTooLongInput, isCaseSesitive }: FuzzyMatchOptions = {},
): FuzzyMatchData | false {
  // make some validation first

  // if input is longer than string to find, and we dont truncate it - it's incorrect
  if (input.length > stringToBeFound.length && !truncateTooLongInput) {
    return false;
  }

  // if truncate is enabled - do it
  if (input.length > stringToBeFound.length && truncateTooLongInput) {
    input = input.substr(0, stringToBeFound.length);
  }

  // if input is the same as string to be found - we dont need to look for fuzzy match - return it as match
  if (input === stringToBeFound) {
    return {
      parts: [{ content: input, type: 'input' }],
      score: 1,
    };
  }

  const matchParts: FuzzyMatchPart[] = [];

  const remainingInputLetters = input.split('');

  // let's create letters buffers
  // it's because we'll perform matching letter by letter, but if we have few letters matching or not matching in the row
  // we want to add them together as part of match
  let ommitedLettersBuffer: string[] = [];
  let matchedLettersBuffer: string[] = [];

  // helper functions to clear the buffers and add them to match
  function addOmmitedLettersAsFuzzy() {
    if (ommitedLettersBuffer.length > 0) {
      matchParts.push({
        content: ommitedLettersBuffer.join(''),
        type: 'fuzzy',
      });
      ommitedLettersBuffer = [];
    }
  }

  function addMatchedLettersAsInput() {
    if (matchedLettersBuffer.length > 0) {
      matchParts.push({
        content: matchedLettersBuffer.join(''),
        type: 'input',
      });
      matchedLettersBuffer = [];
    }
  }

  for (let anotherStringToBeFoundLetter of stringToBeFound) {
    const inputLetterToMatch = remainingInputLetters[0];

    // no more input - finish fuzzy matching
    if (!inputLetterToMatch) {
      break;
    }

    const isMatching = compareLetters(
      anotherStringToBeFoundLetter,
      inputLetterToMatch,
      isCaseSesitive,
    );

    // if input letter doesnt match - we'll go to the next letter to try again
    if (!isMatching) {
      // add this letter to buffer of ommited letters
      ommitedLettersBuffer.push(anotherStringToBeFoundLetter);
      // in case we had something in matched letters buffer - clear it as matching letters run ended
      addMatchedLettersAsInput();
      // go to the next input letter
      continue;
    }

    // we have input letter matching!

    // remove it from remaining input letters
    remainingInputLetters.shift();

    // add it to matched letters buffer
    matchedLettersBuffer.push(anotherStringToBeFoundLetter);
    // in case we had something in ommited letters buffer - add it to the match now
    addOmmitedLettersAsFuzzy();

    // if there is no more letters in input - add this matched letter to match too
    if (!remainingInputLetters.length) {
      addMatchedLettersAsInput();
    }
  }

  // if we still have letters left in input - means not all input was included in string to find - input was incorrect
  if (remainingInputLetters.length > 0) {
    return false;
  }

  // lets get entire matched part (from start to last letter of input)
  const matchedPart = matchParts.map((match) => match.content).join('');

  // get remaining part of string to be found
  const suggestionPart = stringToBeFound.replace(matchedPart, '');

  // if we have remaining part - add it as suggestion
  if (suggestionPart) {
    matchParts.push({ content: suggestionPart, type: 'suggestion' });
  }
  const score = calculateFuzzyMatchPartsScore(matchParts);

  return {
    score,
    parts: matchParts,
  };
}
person Adam Pietrasiak    schedule 07.12.2018