fizzSearch
/customize/fizzSearchに移動した
/icons/hr.icon
あいまい検索を行う関数
UserScriptで使用する
あいまい検索クラス
ambiguous text search
cf.
/shokai/asearch
ソース:node-asearch/asearch.coffee at master · shokai/node-asearch · GitHub
これを/icons/javascript.iconに書き直しただけ
課題
検索を途中で打ち切る
最大n件しか使わないなら、それ以上検索する必要がない
高速化を期待できる
並列で検索している場合も使える
個々のworkerごとに検索数上限を設定するのは計算が難しいが、代わりに最終的な検索数上限をつかえばいい
2020-11-19 08:37:14 やってみた
検索語句に最も関連している順に並び替えしたい
emoji-completionなどで[:box]と打ったときに、先頭にboxが表示されないのが辛い
対策
類似度を数値化し、それを元に並べ替える
レーベンシュタイン距離 - Wikipediaを使うと良さそう
from 文字列の類似度を測る(1) レーベンシュタイン距離|Colorless Green Ideas
2020-10-15 15:27:34 ambigの順番に検索結果を並べ替える
完全一致、一文字間違い、2文字間違い、……と並べることができる
もうちょっと高速化したい
✅fizzSearchを並列化する
/icons/done.iconasearchに部分一致機能を付けたい
検索対象の前後に空白を入れることで解決した
from /villagepump/scrapboxのリンクサジェスト、とても速くないか?#5f62f27edd59fe0000592213
ひらがな/カタカナ/カタカナを区別しないで検索できるようにしたい
検索候補と、検索文字列とを全てひらがな/alphabetに変換してから検索を開始する
全角と半角の変換処理を使って変換する
Asearchに渡す前に変換すればいいかな?
あまり負荷がかかるようであれば、予め利用者側で変換済みの文字列を用意してもらうという手もある
更新とか
2021-02-04 18:37:01 Asearchを少しrefactoringした
2020-11-02 10:38:03
検索keyword or 検索候補がからのときは、0件の検索結果を返すようにした
検索keywordに空白を与えると固まる問題への対処
2020-10-15 15:38:33
あいまい度順に検索結果が並ぶようにした
2020-10-12 01:48:50
funcを引数に追加
ambigの上限値が3になっていたのを4に直した
limitCount <= 4
2020-09-17 20:34:24
歯抜けマッチングを除外した
asearchで部分一致検索する方法がわかったので、歯抜けマッチングを使う必要がなくなった
2020-09-17 11:30:57
Array.prototype.flatMap()で書き換えた
code:script.js
class Asearch {
get INITPAT() {return 0x80000000;} // 0100,0000,0000,0000,0000,0000,0000,0000
get MAXCHAR() {return 0x100;}
get INITSTATE() {return this.INITPAT, 0, 0, 0;}
constructor(source) {
this.source = source;
this.shiftpat = Array(this.MAXCHAR).map(_ => 0);
this.epsilon = 0;
let mask = this.INITPAT;
const ref = this.unpack(this.source);
for (const item of ref) {
// 0x20 is a space
if (item === 0x20) {
this.epsilon |= mask;
} else {
this.shiftpatitem |= mask;
this.shiftpatthis.toupper(item) |= mask;
this.shiftpatthis.tolower(item) |= mask;
mask >>>= 1;
}
}
this.acceptpat = mask;
}
isupper(c) {
// 0x41 = A, 0x5a = Z
return (c >= 0x41) && (c <= 0x5a);
}
islower(c) {
// 0x61 = a, 0x7a = z
return (c >= 0x61) && (c <= 0x7a);
}
tolower(c) {
return this.isupper(c) ? c + 0x20 : c;
}
toupper(c) {
return this.islower(c) ? c - 0x20 : c;
}
state(str = '') {
let i0 = this.INITSTATE0;
let i1 = this.INITSTATE1;
let i2 = this.INITSTATE2;
let i3 = this.INITSTATE3;
const ref = this.unpack(str);
for (const item of ref) {
const mask = this.shiftpatitem;
i3 = (i3 & this.epsilon) | ((i3 & mask) >>> 1) | (i2 >>> 1) | i2;
i2 = (i2 & this.epsilon) | ((i2 & mask) >>> 1) | (i1 >>> 1) | i1;
i1 = (i1 & this.epsilon) | ((i1 & mask) >>> 1) | (i0 >>> 1) | i0;
i0 = (i0 & this.epsilon) | ((i0 & mask) >>> 1);
i1 |= i0 >>> 1;
i2 |= i1 >>> 1;
i3 |= i2 >>> 1;
}
return i0, i1, i2, i3;
}
match(str, ambig = 0) {
if (!(ambig < this.INITSTATE.length)) {
ambig = this.INITSTATE.length - 1;
}
const s = this.state(str);
return (sambig & this.acceptpat) !== 0;
}
unpack(str) {
return str.split('')
.flatMap(char => {
const code = char.charCodeAt(0);
if (code > 0xFF) {
return (code & 0xFF00) >>> 8, code & 0xFF;
}
return code & 0xFF;
});
}
}
解説:/masui/曖昧検索asearch
一文字誤りのpatternを検出できる
文字抜け
余計な文字が入る
文字が間違っている
この解説を読んだだけではtakker.iconの頭ではわからない
少しずつ自分の言葉で噛み砕く必要がある
本体
word:検索したい語句
list:検索候補
func: listの中身を加工して渡したいときにつかう函数
2020-11-26 09:32:43 fizzSearchの方、曖昧度別に分けられていなかったや
修正しよう
code:script.js
const range = n => ...Array(n).keys(); // 0,1,2,...,n-1を返す
function fizzSearch({word, list, func = w => w, limit = undefined} = {}) {
// 検索keyword及び検索候補が空のときは何もしない
if (word.trim() === '' || list.length === 0) return [];
// 空白文字で区切った文字列を並び替える
const permutation = getPermutation(word.split(/\s/)).map(wordList => ${wordList.join(' ')} );
// ambigの最大値を計算する
// permutationは、文字列を並び替えただけなので、どれも長さは一緒
let maxAmbig = Math.floor(permutation0.length / 4) + 1;
maxAmbig = maxAmbig > 4 ? 4 : maxAmbig;
// ambigの値に応じて検索する
// limit < list.lengthなら検索を打ち切る
const results = [];
let matchedNum = 0;
(() => {
for (const ambig of range(maxAmbig)) {
let matches = new Set();
for (const targetWord of permutation) {
const prevSize = matches.size;
const asearch = new Asearch(targetWord);
// 検索したものを重複を取り除いて追加する
const temp = list.filter(word => asearch.match(func(word), ambig));
temp.forEach(word => matches.add(word));
matchedNum += matches.size - prevSize;
if (limit && matchedNum > limit) {
results.push(...matches);
return;
}
}
results.push(...matches);
}
})();
// 重複を取り除いて返す
return results.map((list, i, lists) => i === 0 ?
list:
list.filter(word => !listsi-1.includes(word)));
}
function fizzSearch2(word, list, func = w => w) {
// 検索keyword及び検索候補が空のときは何もしない
if (word.trim() === '' || list.length === 0) return [];
// 空白文字で区切った文字列を並び替える
const permutation = getPermutation(word.split(/\s/)).map(wordList => ${wordList.join(' ')} );
// ambigの最大値を計算する
// permutationは、文字列を並び替えただけなので、どれも長さは一緒
let maxAmbig = Math.floor(permutation0.length / 4) + 1;
maxAmbig = maxAmbig > 4 ? 4 : maxAmbig;
// ambigの値に応じて検索する
const searchedLists = range(maxAmbig)
.map(i => permutation.flatMap(targetWord =>{
const asearch = new Asearch(targetWord);
return list.filter(word => asearch.match(func(word), i));
}))
// 重複を取り除く
.map((list, i, lists) => i === 0 ? list : list.filter(word => !listsi-1.includes(word)))
.map(list => ...new Set(list));
//const searchedList = permutation
// .flatMap(targetWord => ...asearched(targetWord), ...hanukeSearch(targetWord, list));
return searchedLists;
}
ESModule用
code:import.js
import '/api/code/takker/fizzSearch/script.js';
export default function(word, list, func = w => w) {
return fizzSearch(word, list, func);
}
↓現在使っていない
歯抜けマッチング
cf. /shokai/taberareloo
scrと入力すると/s.*c.*r.*/という正規表現にマッチする文字列を返す
途中まで入力すればマッチする
所々抜けがあってもマッチする
code:script.js
// wordの歯抜け検索をlistから行い、matchした文字列のリストを返す
function hanukeSearch(word, list) {
const regexString = word
.match(/./ug)// 書記素ごとに分割
.map(char => char.replace(/[.*+?^=!:${}()|\\/\\]/ug, '\\$&'))// 正規表現のescape
.map(char => ${char}.*)
.join('');
const reg = RegExp(regexString, 'ui');
return list.filter(title => reg.test(title));
}
全順序マッチング
/shokai/array-permutation-simple
順列を作成する関数を実装すれば良い
code:script.js
// 重複は考慮していない
function getPermutation(list) {
if (list.length == 0) return list;
if (list.length == 1) return list;
if (list.length == 2) return list0,list[1,[list1,list0]];
return list.flatMap(first => {
const restList = list.filter(item => item !== first);
return getPermutation(restList).map(permutation => first, ...permutation);
});
}
#2021-02-04 18:37:20
#2020-11-25 11:38:15
#2020-11-19
#2020-11-02 10:39:11
#2020-10-15 15:27:43
#2020-10-12 01:47:53
#2020-09-17
#2020-09-14
#2020-08-24