Крошечный, эффективный нечеткий поиск, который не отстой. Это мой пушистик?. Таких много, но этот мой.¹
uFuzzy — это библиотека нечеткого поиска, предназначенная для сопоставления относительно короткой поисковой фразы (иглы) с большим списком коротких и средних фраз (стог сена). Его лучше всего можно описать как более щадящий метод String.includes(). Общие приложения включают фильтрацию списков, автозаполнение/предложение, а также поиск заголовков, имен, описаний, имен файлов и функций.
В режиме MultiInsert
uFuzzy по умолчанию каждое совпадение должно содержать все буквенно-цифровые символы от иглы в одной и той же последовательности; в режиме SingleError
в каждом термине допускаются единичные опечатки (расстояние Дамерау–Левенштейна = 1). Его API .search()
может эффективно сопоставлять термины, выходящие за пределы порядка, поддерживает множественные исключения подстрок (например, fruit -green -melon
) и точные термины с небуквенными символами (например, "C++"
, "$100"
, "#hashtag"
). Если держать его правильно , он также может эффективно сопоставлять несколько свойств объекта.
Array.sort()
, который получает доступ к статистике/счетчикам каждого совпадения. Не существует единого «счета» черного ящика, который нужно было бы понять.uFuzzy оптимизирован для латинского/латинского алфавита и внутренне использует регулярные выражения, не поддерживающие Юникод.
Поддержка большего количества языков осуществляется путем дополнения встроенных латинских регулярных выражений дополнительными символами или использования более медленного универсального варианта {unicode: true}
. Более простая, но менее гибкая альтернатива {alpha: "..."}
заменяет части AZ
и az
встроенных латинских регулярных выражений символами по вашему выбору (регистр букв будет автоматически сопоставляться во время замены).
Утилитную функцию uFuzzy.latinize()
можно использовать для удаления общих акцентов/диакритических знаков из стога сена и иголки перед поиском.
// Latin (default)
let opts = { alpha : "a-z" } ;
// OR
let opts = {
// case-sensitive regexps
interSplit : "[^A-Za-z\d']+" ,
intraSplit : "[a-z][A-Z]" ,
intraBound : "[A-Za-z]\d|\d[A-Za-z]|[a-z][A-Z]" ,
// case-insensitive regexps
intraChars : "[a-z\d']" ,
intraContr : "'[a-z]{1,2}\b" ,
} ;
// Latin + Norwegian
let opts = { alpha : "a-zæøå" } ;
// OR
let opts = {
interSplit : "[^A-Za-zæøåÆØÅ\d']+" ,
intraSplit : "[a-zæøå][A-ZÆØÅ]" ,
intraBound : "[A-Za-zæøåÆØÅ]\d|\d[A-Za-zæøåÆØÅ]|[a-zæøå][A-ZÆØÅ]" ,
intraChars : "[a-zæøå\d']" ,
intraContr : "'[a-zæøå]{1,2}\b" ,
} ;
// Latin + Russian
let opts = { alpha : "a-zа-яё" } ;
// OR
let opts = {
interSplit : "[^A-Za-zА-ЯЁа-яё\d']+" ,
intraSplit : "[a-z][A-Z]|[а-яё][А-ЯЁ]" ,
intraBound : "[A-Za-zА-ЯЁа-яё]\d|\d[A-Za-zА-ЯЁа-яё]|[a-z][A-Z]|[а-яё][А-ЯЁ]" ,
intraChars : "[a-zа-яё\d']" ,
intraContr : "'[a-z]{1,2}\b" ,
} ;
// Unicode / universal (50%-75% slower)
let opts = {
unicode : true ,
interSplit : "[^\p{L}\d']+" ,
intraSplit : "\p{Ll}\p{Lu}" ,
intraBound : "\p{L}\d|\d\p{L}|\p{Ll}\p{Lu}" ,
intraChars : "[\p{L}\d']" ,
intraContr : "'\p{L}{1,2}\b" ,
} ;
Все поисковые запросы в настоящее время нечувствительны к регистру; невозможно выполнить поиск с учетом регистра.
ПРИМЕЧАНИЕ. Файл testdata.json представляет собой разнообразный набор данных из 162 000 строк и фраз размером 4 МБ, поэтому первая загрузка может быть медленной из-за сетевой передачи. Попробуйте обновить данные, как только они будут кэшированы вашим браузером.
Во-первых, uFuzzy изолированно, чтобы продемонстрировать свою производительность.
https://leoniya.github.io/uFuzzy/demos/compare.html?libs=uFuzzy&search=super%20ma
Теперь та же страница сравнения, загруженная с помощью fuzzysort, QuickScore и Fuse.js:
https://leoniya.github.io/uFuzzy/demos/compare.html?libs=uFuzzy,fuzzysort,QuickScore,Fuse&search=super%20ma
Вот полный список библиотек, но с сокращенным набором данных (просто hearthstone_750
, urls_and_titles_600
), чтобы избежать сбоя вашего браузера:
https://leoniya.github.io/uFuzzy/demos/compare.html?lists=hearthstone_750,urls_and_titles_600&search=moo
Ответы:
Остальное: https://github.com/leeoniya/uFuzzy/issues.
npm i @leeoniya/ufuzzy
const uFuzzy = require ( '@leeoniya/ufuzzy' ) ;
< script src = "./dist/uFuzzy.iife.min.js" > < / script >
let haystack = [
'puzzle' ,
'Super Awesome Thing (now with stuff!)' ,
'FileName.js' ,
'/feeding/the/catPic.jpg' ,
] ;
let needle = 'feed cat' ;
let opts = { } ;
let uf = new uFuzzy ( opts ) ;
// pre-filter
let idxs = uf . filter ( haystack , needle ) ;
// idxs can be null when the needle is non-searchable (has no alpha-numeric chars)
if ( idxs != null && idxs . length > 0 ) {
// sort/rank only when <= 1,000 items
let infoThresh = 1e3 ;
if ( idxs . length <= infoThresh ) {
let info = uf . info ( idxs , haystack , needle ) ;
// order is a double-indirection array (a re-order of the passed-in idxs)
// this allows corresponding info to be grabbed directly by idx, if needed
let order = uf . sort ( info , haystack , needle ) ;
// render post-filtered & ordered matches
for ( let i = 0 ; i < order . length ; i ++ ) {
// using info.idx here instead of idxs because uf.info() may have
// further reduced the initial idxs based on prefix/suffix rules
console . log ( haystack [ info . idx [ order [ i ] ] ] ) ;
}
}
else {
// render pre-filtered but unordered matches
for ( let i = 0 ; i < idxs . length ; i ++ ) {
console . log ( haystack [ idxs [ i ] ] ) ;
}
}
}
uFuzzy предоставляет оболочку uf.search(haystack, needle, outOfOrder = 0, infoThresh = 1e3) => [idxs, info, order]
которая объединяет описанные выше шаги filter
, info
и sort
. Этот метод также реализует эффективную логику для сопоставления условий поиска не по порядку и поддержку нескольких исключений подстрок, например, fruit -green -melon
.
Сначала получите заказанные спички:
let haystack = [
'foo' ,
'bar' ,
'cowbaz' ,
] ;
let needle = 'ba' ;
let u = new uFuzzy ( ) ;
let idxs = u . filter ( haystack , needle ) ;
let info = u . info ( idxs , haystack , needle ) ;
let order = u . sort ( info , haystack , needle ) ;
Базовый маркер внутреннего HTML (диапазоны, заключенные в <mark>
):
let innerHTML = '' ;
for ( let i = 0 ; i < order . length ; i ++ ) {
let infoIdx = order [ i ] ;
innerHTML += uFuzzy . highlight (
haystack [ info . idx [ infoIdx ] ] ,
info . ranges [ infoIdx ] ,
) + '<br>' ;
}
console . log ( innerHTML ) ;
Подсветка InternalHTML с настраиваемой функцией маркировки (диапазоны, заключенные в <b>
):
let innerHTML = '' ;
const mark = ( part , matched ) => matched ? '<b>' + part + '</b>' : part ;
for ( let i = 0 ; i < order . length ; i ++ ) {
let infoIdx = order [ i ] ;
innerHTML += uFuzzy . highlight (
haystack [ info . idx [ infoIdx ] ] ,
info . ranges [ infoIdx ] ,
mark ,
) + '<br>' ;
}
console . log ( innerHTML ) ;
Подсветка элементов DOM/JSX с настраиваемой маркировкой и функциями добавления:
let domElems = [ ] ;
const mark = ( part , matched ) => {
let el = matched ? document . createElement ( 'mark' ) : document . createElement ( 'span' ) ;
el . textContent = part ;
return el ;
} ;
const append = ( accum , part ) => { accum . push ( part ) ; } ;
for ( let i = 0 ; i < order . length ; i ++ ) {
let infoIdx = order [ i ] ;
let matchEl = document . createElement ( 'div' ) ;
let parts = uFuzzy . highlight (
haystack [ info . idx [ infoIdx ] ] ,
info . ranges [ infoIdx ] ,
mark ,
[ ] ,
append ,
) ;
matchEl . append ( ... parts ) ;
domElems . push ( matchEl ) ;
}
document . getElementById ( 'matches' ) . append ( ... domElems ) ;
uFuzzy имеет два режима работы, которые различаются стратегией сопоставления:
example
- точныйexamplle
— одиночная вставка (дополнение)exemple
- одиночная замена (замена)exmaple
- одиночная транспозиция (своп)exmple
– однократное удаление (пропуск)xamp
— частичныйxmap
— частичный с транспонированиемПоиск состоит из 3 этапов:
haystack
с помощью быстрого RegExp, скомпилированного из вашей needle
без каких-либо дополнительных операций. Он возвращает массив совпадающих индексов в исходном порядке.needle
в два более дорогих RegExp, которые могут разделить каждое совпадение. Поэтому его следует запускать на сокращенном подмножестве стога сена, обычно возвращаемом на этапе фильтрации. Демо-версия uFuzzy ограничена <= 1000 отфильтрованными элементами, прежде чем переходить к этому этапу.Array.sort()
для определения окончательного порядка результатов, используя info
объект, возвращенный с предыдущего этапа. Пользовательскую функцию сортировки можно предоставить с помощью опции uFuzzy: {sort: (info, haystack, needle) => idxsOrder}
.Файл uFuzzy.d.ts с щедрыми комментариями 200 LoC.
Опции с префиксом inter применяются к допускам между условиями поиска, а варианты с префиксом внутри применяются к допускам внутри каждого условия поиска.
Вариант | Описание | По умолчанию | Примеры |
---|---|---|---|
intraMode | Как следует выполнять сопоставление терминов | 0 | 0 Мультивставка1 одиночная ошибкаПосмотрите, как это работает |
intraIns | Максимально допустимое количество дополнительных символов между каждым символом в течение срока | Соответствует значению intraMode ( 0 или 1 ). | Ищу "кот"...0 может соответствовать: cat , s cat , cat ch, va cat e1 также соответствует: car t , глава , out cast |
interIns | Максимальное количество дополнительных символов, разрешенное между терминами | Infinity | В поисках "где находится"...Infinity может соответствовать: где , где есть бла, это дом5 не может совпадать: где тут бла мудрость |
intraSub intraTrn intraDel | Для intraMode: 1 ,Типы ошибок, которые следует допускать в рамках условий | Соответствует значению intraMode ( 0 или 1 ). | 0 Нет1 Да |
intraChars | Частичное регулярное выражение для разрешенной вставки символы между каждым символом в термине | [azd'] | [azd] соответствует только буквенно-цифровым значениям (без учета регистра)[w-] будет соответствовать буквенно-цифровому, подчеркиванию и дефису. |
intraFilt | Обратный вызов для исключения результатов на основе термина и соответствия | (term, match, index) => true | Может быть, займитесь своим делом... - Порог разницы длин - расстояние Левенштейна - Смещение срока или содержание |
interChars | Частичное регулярное выражение для разрешенных символов между терминами | . | . соответствует всем символам[^azd] будет соответствовать только пробелам и знакам препинания. |
interLft | Определяет допустимую левую границу термина | 0 | В поисках "мания"...0 любой - где угодно: ro mania n1 свободное пространство — пробелы, знаки препинания, буквы и цифры, переходы с изменением регистра: Track Mania , mania c2 строгий – пробелы, пунктуация: маниакально |
interRgt | Определяет правую границу допустимого срока | 0 | В поисках "мания"...0 любой - где угодно: ro mania n1 свободное место — пробелы, знаки препинания, буквы и цифры, переходы с изменением регистра: Mania Star.2 строгий — пробелы, пунктуация: mania _foo |
sort | Пользовательская функция сортировки результатов | (info, haystack, needle) => idxsOrder | По умолчанию: сортировка поиска с приоритетом полных совпадений терминов и плотности символов. Демо: сортировка по опережающему вводу, приоритет начального смещения и длины совпадения |
Эта оценка чрезвычайно узка и, конечно же, предвзята к моим вариантам использования, текстовому корпусу и моему полному опыту работы с собственной библиотекой. Весьма вероятно, что я не использую в полной мере преимущества некоторых функций других библиотек, которые могут значительно улучшить результаты по некоторым направлениям; Я приветствую PR по улучшению от любого, у кого более глубокие знания библиотеки, чем те, что я могу получить в результате моего беглого 10-минутного просмотра любого примера «Базового использования» и документа README.
Банка с червями №1.
Прежде чем мы обсудим производительность, давайте поговорим о качестве поиска, потому что скорость не имеет значения, когда ваши результаты представляют собой странную смесь «О да!» и «Что за черт?».
Качество поиска очень субъективно. То, что представляет собой хорошее совпадение с верхним числом в случае «упреждающего ввода/автоматического предложения», может оказаться плохим совпадением в сценарии «поиска/найти все». Некоторые решения оптимизируются для последнего, некоторые — для первого. Обычно можно встретить ручки, которые искажают результаты в любом направлении, но они часто бывают несовершенными и несовершенными, являясь не чем иным, как прокси-сервером для получения единого, составного «счета» совпадения.
ОБНОВЛЕНИЕ (2024 г.). Приведенная ниже критика странных совпадений справедлива только для конфигурации Fuse.js по умолчанию . Как ни странно, установка ignoreFieldNorm: true
значительно улучшила результаты, но порядок совпадений высокого качества остается неудовлетворительным.
Давайте посмотрим на некоторые совпадения, созданные самой популярной библиотекой нечеткого поиска Fuse.js и некоторыми другими, для которых в демо-версии реализована подсветка совпадений.
При поиске частичного термина «сумерки» мы видим, что эти результаты появляются над многочисленными очевидными результатами «сумерек» :
https://leoniya.github.io/uFuzzy/demos/compare.html?libs=uFuzzy,fuzzysort,QuickScore,Fuse&search=twili
Эти плохие совпадения не только изолированы, но и имеют более высокий рейтинг, чем буквальные подстроки.
Если завершить поисковый запрос до «сумерек» , странные результаты по- прежнему будут выше:
https://leoniya.github.io/uFuzzy/demos/compare.html?libs=uFuzzy,fuzzysort,QuickScore,Fuse&search=twilight
Некоторые движки лучше справляются с частичным совпадением префиксов за счет более высоких затрат на запуск/индексирование:
https://leoniya.github.io/uFuzzy/demos/compare.html?libs=uFuzzy,FlexSearch,match-sorter,MiniSearch&search=twili
Здесь match-sorter
возвращает 1384 результата, но релевантны только первые 40. Как мы узнаем, где находится граница?
Банка с червями №2.
Все тесты отстойные, но этот может отстойнее других.
Тем не менее, что-то лучше, чем увольнение по принципу YMMV/сделай сам, и уж точно лучше, чем ничего.
Среда
Дата | 2023-10 |
---|---|
Аппаратное обеспечение | Процессор: Ryzen 7 PRO 5850U (1,9 ГГц, 7 нм, TDP 15 Вт) ОЗУ: 48 ГБ SSD: Samsung SSD 980 PRO 1 ТБ (NVMe) |
ОС | EndeavourOS (Arch Linux) v6.5.4-arch2-1 x86_64 |
Хром | v117.0.5938.132 |
libs
на желаемое имя библиотеки: https://leeoniya.github.io/uFuzzy/demos/compare.html?bench&libs=uFuzzy.bench
режиме, чтобы избежать тестирования DOM.test
, chest
, super ma
, mania
, puzz
, prom rem stor
, twil
. Чтобы оценить результаты для каждой библиотеки или сравнить несколько, просто посетите ту же страницу с большим количеством libs
и без bench
: https://leoniya.github.io/uFuzzy/demos/compare.html?libs=uFuzzy,fuzzysort,QuickScore ,Fuse&search=super%20ma.
Оценивается несколько показателей:
Либ | Звезды | Размер (мин) | Инициализировать | Поиск (х 86) | Куча (пик) | Сохранено | ГК |
---|---|---|---|---|---|---|---|
уФуззи (попробуй) | ★ 2,3 тыс. | 7,6 КБ | 0,5 мс | 434 мс | 28,4 МБ | 7,4 МБ | 18 мс |
уФуззи (попробуй) (внешнее кэширование префиксов) | 210 мс | 27,8 МБ | 7,4 МБ | 18 мс | |||
уФуззи (попробуй) (outOfOrder, более нечеткий) | 545 мс | 29,5 МБ | 7,4 МБ | 18 мс | |||
уФуззи (попробуй) (outOfOrder, нечеткий, SingleError) | 508 мс | 30,0 МБ | 7,4 МБ | 18 мс | |||
------- | |||||||
Fuse.js (попробуйте) | ★ 16,6 тыс. | 24,2 КБ | 31 мс | 33875 мс | 245 МБ | 13,9 МБ | 25 мс |
FlexSearch (Light) (попробуйте) | ★ 10,7 тыс. | 6,2 КБ | 3210 мс | 83 мс | 670 МБ | 316 МБ | 553 мс |
Lunr.js (попробуйте) | ★ 8,7 тыс. | 29,4 КБ | 1704 мс | 996 мс | 380 МБ | 123 МБ | 166 мс |
Орама (ранее Лира) (попробуй) | ★ 6,4к | 41,5 КБ | 2650 мс | 225 мс | 313 МБ | 192 МБ | 180 мс |
Минипоиск (попробуйте) | ★ 3,4к | 29,1 КБ | 504 мс | 1453 мс | 438 МБ | 67 МБ | 105 мс |
сортировщик спичек (попробуйте) | ★ 3,4к | 7,3 КБ | 0,1 мс | 6245 мс | 71 МБ | 7,3 МБ | 12 мс |
нечеткая сортировка (попробуйте) | ★ 3,4к | 6,2 КБ | 50 мс | 1321 мс | 175 МБ | 84 МБ | 63 мс |
Уэйд (попробуй) | ★ 3к | 4 КБ | 781 мс | 194 мс | 438 МБ | 42 МБ | 130 мс |
нечеткий поиск (попробуйте) | ★ 2,7 тыс. | 0,2 КБ | 0,1 мс | 529 мс | 26,2 МБ | 7,3 МБ | 18 мс |
js-поиск (попробуйте) | ★ 2,1 тыс. | 17,1 КБ | 5620 мс | 1190 мс | 1740 МБ | 734 МБ | 2600 мс |
Elasticlunr.js (попробуйте) | ★ 2к | 18,1 КБ | 933 мс | 1330 мс | 196 МБ | 70 МБ | 135 мс |
Фаззисет (попробуйте) | ★ 1,4к | 2,8 КБ | 2962 мс | 606 мс | 654 МБ | 238 МБ | 239 мс |
индекс поиска (попробуйте) | ★ 1,4к | 168 КБ | RangeError: превышен максимальный размер стека вызовов | ||||
sifter.js (попробуйте) | ★ 1,1 тыс. | 7,5 КБ | 3 мс | 1070 мс | 46,2 МБ | 10,6 МБ | 18 мс |
fzf-for-js (попробуйте) | ★ 831 | 15,4 КБ | 50 мс | 6290 мс | 153 МБ | 25 МБ | 18 мс |
нечетко (попробуй) | ★ 819 | 1,4 КБ | 0,1 мс | 5427 мс | 72 МБ | 7,3 МБ | 14 мс |
быстро-нечеткий (попробуй) | ★ 346 | 18,2 КБ | 790 мс | 19266 мс | 550 МБ | 165 МБ | 140 мс |
ItemsJS (попробуйте) | ★ 305 | 109 КБ | 2400 мс | 11304 мс | 320 МБ | 88 МБ | 163 мс |
Жидкий Металл (попробуйте) | ★ 292 | 4,2 КБ | (крушение) | ||||
Нечеткий поиск (попробуйте) | ★ 209 | 3,5 КБ | 2 мс | 3948 мс | 84 МБ | 10,5 МБ | 18 мс |
FuzzySearch2 (попробуйте) | ★ 186 | 19,4 КБ | 93 мс | 4189 мс | 117 МБ | 40,3 МБ | 40 мс |
QuickScore (попробуйте) | ★ 153 | 9,5 КБ | 10 мс | 6915 мс | 133 МБ | 12,1 МБ | 18 мс |
ndx (попробуй) | ★ 142 | 2,9 КБ | 300 мс | 581 мс | 308 МБ | 137 МБ | 262 мс |
фзи (попробуй) | ★ 133 | 1,5 КБ | 0,1 мс | 3932 мс | 34 МБ | 7,3 МБ | 10 мс |
нечеткие инструменты (попробуйте) | ★ 13 | 3 КБ | 0,1 мс | 5138 мс | 164 МБ | 7,5 МБ | 18 мс |
fuzzyMatch (попробуйте) | ★ 0 | 1 КБ | 0,1 мс | 2415 мс | 83,5 МБ | 7,3 МБ | 13 мс |