select2で崇高なファジー検索を実装するにはどうすればよいですか?
たとえば、「sta java sub」と入力すると、「Stackoverflow javascript sublime like」と一致します。
select2で崇高なファジー検索を実装するにはどうすればよいですか?
たとえば、「sta java sub」と入力すると、「Stackoverflow javascript sublime like」と一致します。
別のマッチング関数を次に示します。http://jsfiddle.net/trevordixon/pXzj3/4/
function match(search, text) {
search = search.toUpperCase();
text = text.toUpperCase();
var j = -1; // remembers position of last found character
// consider each search character one at a time
for (var i = 0; i < search.length; i++) {
var l = search[i];
if (l == ' ') continue; // ignore spaces
j = text.indexOf(l, j+1); // search for character & update position
if (j == -1) return false; // if it's not found, exclude this item
}
return true;
}
これは(Chromeでのこのテストによると)より高速であり、多くのアイテムをフィルタリングしている場合に問題になり始める可能性があります.
select2 を使用すると、独自の「マッチャー」関数 ( docs に見られるように)を実装できます。それといくつかの正規表現を使用して、次のようなことができます。
$("#element").select2({
matcher: function(term, text, opt) {
//We call to uppercase to do a case insensitive match
//We replace every group of whitespace characters with a .+
//matching any number of characters
return text.toUpperCase().match(term.toUpperCase().replace(/\s+/g, '.+'));
}
});
リストをフィルタリング/検索するときに、すべての select2 リスト要素に対してマッチャー関数が呼び出されます。それを使用して、任意の種類のカスタム検索を実装できます。
Sublime Text のファジー マッチが非常に長く機能するものを書きました。これを実現するには、いくつかのことが必要です。
まず、パターンのすべての文字を順番に一致させます。次に、特定の一致した文字が他の文字よりも多くのポイントに値するように一致をスコアリングします。
私はチェックするいくつかの要因を思いつきました. 「CamelCase」文字または区切り記号 (スペースまたはアンダースコア) に続く文字は、多くのポイントに値します。連続試合はより価値があります。開始近くで見つかった結果は、より価値があります。
非常に重要なトリックは、最も一致する文字を見つけることです。これは必ずしも最初のものではありません。fuzzy_match("tk", "The Black Knight") を検討してください。一致する可能性のある 2 つの K があります。2 番目は、スペースに続くため、より多くのポイントに値します。
JavaScript コードは以下のとおりです。ブログ投稿でより詳細に説明されているいくつかのニュアンスがあります。インタラクティブなデモもあります。完全なソース (デモと C++ 実装を含む) は GitHub にあります。
// Returns [bool, score, formattedStr]
// bool: true if each character in pattern is found sequentially within str
// score: integer; higher is better match. Value has no intrinsic meaning. Range varies with pattern.
// Can only compare scores with same search pattern.
// formattedStr: input str with matched characters marked in <b> tags. Delete if unwanted.
function fuzzy_match(pattern, str) {
// Score consts
var adjacency_bonus = 5; // bonus for adjacent matches
var separator_bonus = 10; // bonus if match occurs after a separator
var camel_bonus = 10; // bonus if match is uppercase and prev is lower
var leading_letter_penalty = -3; // penalty applied for every letter in str before the first match
var max_leading_letter_penalty = -9; // maximum penalty for leading letters
var unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter
// Loop variables
var score = 0;
var patternIdx = 0;
var patternLength = pattern.length;
var strIdx = 0;
var strLength = str.length;
var prevMatched = false;
var prevLower = false;
var prevSeparator = true; // true so if first letter match gets separator bonus
// Use "best" matched letter if multiple string letters match the pattern
var bestLetter = null;
var bestLower = null;
var bestLetterIdx = null;
var bestLetterScore = 0;
var matchedIndices = [];
// Loop over strings
while (strIdx != strLength) {
var patternChar = patternIdx != patternLength ? pattern.charAt(patternIdx) : null;
var strChar = str.charAt(strIdx);
var patternLower = patternChar != null ? patternChar.toLowerCase() : null;
var strLower = strChar.toLowerCase();
var strUpper = strChar.toUpperCase();
var nextMatch = patternChar && patternLower == strLower;
var rematch = bestLetter && bestLower == strLower;
var advanced = nextMatch && bestLetter;
var patternRepeat = bestLetter && patternChar && bestLower == patternLower;
if (advanced || patternRepeat) {
score += bestLetterScore;
matchedIndices.push(bestLetterIdx);
bestLetter = null;
bestLower = null;
bestLetterIdx = null;
bestLetterScore = 0;
}
if (nextMatch || rematch) {
var newScore = 0;
// Apply penalty for each letter before the first pattern match
// Note: std::max because penalties are negative values. So max is smallest penalty.
if (patternIdx == 0) {
var penalty = Math.max(strIdx * leading_letter_penalty, max_leading_letter_penalty);
score += penalty;
}
// Apply bonus for consecutive bonuses
if (prevMatched)
newScore += adjacency_bonus;
// Apply bonus for matches after a separator
if (prevSeparator)
newScore += separator_bonus;
// Apply bonus across camel case boundaries. Includes "clever" isLetter check.
if (prevLower && strChar == strUpper && strLower != strUpper)
newScore += camel_bonus;
// Update patter index IFF the next pattern letter was matched
if (nextMatch)
++patternIdx;
// Update best letter in str which may be for a "next" letter or a "rematch"
if (newScore >= bestLetterScore) {
// Apply penalty for now skipped letter
if (bestLetter != null)
score += unmatched_letter_penalty;
bestLetter = strChar;
bestLower = bestLetter.toLowerCase();
bestLetterIdx = strIdx;
bestLetterScore = newScore;
}
prevMatched = true;
}
else {
// Append unmatch characters
formattedStr += strChar;
score += unmatched_letter_penalty;
prevMatched = false;
}
// Includes "clever" isLetter check.
prevLower = strChar == strLower && strLower != strUpper;
prevSeparator = strChar == '_' || strChar == ' ';
++strIdx;
}
// Apply score for last match
if (bestLetter) {
score += bestLetterScore;
matchedIndices.push(bestLetterIdx);
}
// Finish out formatted string after last pattern matched
// Build formated string based on matched letters
var formattedStr = "";
var lastIdx = 0;
for (var i = 0; i < matchedIndices.length; ++i) {
var idx = matchedIndices[i];
formattedStr += str.substr(lastIdx, idx - lastIdx) + "<b>" + str.charAt(idx) + "</b>";
lastIdx = idx + 1;
}
formattedStr += str.substr(lastIdx, str.length - lastIdx);
var matched = patternIdx == patternLength;
return [matched, score, formattedStr];
}
function fuzzyMe(term, query) {
var score = 0;
var termLength = term.length;
var queryLength = query.length;
var highlighting = '';
var ti = 0;
// -1 would not work as this would break the calculations of bonus
// points for subsequent character matches. Something like
// Number.MIN_VALUE would be more appropriate, but unfortunately
// Number.MIN_VALUE + 1 equals 1...
var previousMatchingCharacter = -2;
for (var qi = 0; qi < queryLength && ti < termLength; qi++) {
var qc = query.charAt(qi);
var lowerQc = qc.toLowerCase();
for (; ti < termLength; ti++) {
var tc = term.charAt(ti);
if (lowerQc === tc.toLowerCase()) {
score++;
if ((previousMatchingCharacter + 1) === ti) {
score += 2;
}
highlighting += "<em>" + tc + "</em>";
previousMatchingCharacter = ti;
ti++;
break;
} else {
highlighting += tc;
}
}
}
highlighting += term.substring(ti, term.length);
return {
score: score,
term: term,
query: query,
highlightedTerm: highlighting
};
}
上記はあいまいさを処理します。次に、選択した 2 つの要素すべてを反復するだけです。
$("#element").select2({
matcher: function(term, text, opt) {
return fuzzyMe(term, text).highlightedTerm;
}
});
ファジー コードの功績 -: https://github.com/brikkens/fuzzy.js