728x90
반응형
G
(Go) 키를 누르시거나 오른쪽 위의 Fuzzy search 버튼을 눌러서 이것저것 검색어 넣고 경험해 보시길. 아니면 Recoeve.net (/user/kipid?cat=[Music/Break]--K-pop) 가셔서 Fuzzy search (검색: Go)
버튼이나 ToR (목록: Tables of Recos)
버튼 누르셔서 경험해 보셔도 좋을듯.
내가 힌트를 얻은 곳은 sublime text 이고, 어딘가에 공개된 code 도 있을것 같지만 베끼기도 귀찮고 내가 쓰는 용도에 최적화 하기도 힘들것 같아서 직접 짜보기로 결심. 뭐 크게 어려울거 같지는 않고, 짜는 과정에서 배우는 것도 많을거 같아서...
Google 에서 "Fuzzy search", "Approximate string matching" , "Fuzzy search sublime text" 등을 간단히 검색해보긴 했는데, 딱히 구체적인 방법론이나 code 를 posting 해준데는 없는거 같음. 한글 초성검색과도 관련 있긴 함. 내 코드는 초중종성 따지지 않고 들어가 있기만 하다면 모음이든 자음이든 검색해주긴 하지만...
## TOC
##[.no-sec-N#sec-Progress] Progress
우선 기본적인 구현은 https://kipid.tistory.com/entry/Lists 에서 해놨는데....
앞으로 고치고 싶은것들 리스트 좀 여기다가 정리해 놓으면서 업데이트 해야겠음.
### To do
- Sorting by DESC MMS 를 insertion sort 로 했는데, 이거 더 빠른 sort 로 바꾸긴 해야할듯? (뭐 정렬해야하는 갯수가 그리 많은건 아니라서 이정도면 충분할거 같기도 하지만...)
- 마우스로 복붙할때도 제대로 동작하도록 만들자. : on "input keyup cut paste" with clearTimeout/setTimeout 으로 해결했음. 그냥 on "input" 만으로 되는건데, IE 에서까지 제대로 동작하게 하느라 좀 복잡해짐 ㅡㅡ. 그지같은 IE.
m.fsResult
만 저장해놓고m.fsFullList
가지고 fuzzy search 하는 식인데... 이게 fsFullList 를 바꾸고 싶을때도 있을테니,m.fuzzySearch(pattern, subList)
돌아가는 방식을 조금 더 일반화 해놓을 필요가 있을듯.
한글의 경우 "각" 은 "ㄱㅏㄱ" 과 같이 풀어서 생각. (한글과 비슷하게 풀어서 생각할 수 있는 언어들이 또 있나? 일본어랑 중국어???) 영어 "ABC" 는 이미 풀어져 있음. 단 영어와 같은 latin 계열 언어는 ignore case 옵션이 붙을 수 있음.
String $S$ 안에 pattern $P$ 에 해당하는 letter 들이 match 되어야 하고, match score 는 인접한 match 간 간격이 좁을수록 높아지는 형태로 디자인하자.
$P$ 를 $S$ 에 match 시켰을 때의 각 pattern 의 match 위치를 $m_1$, $m_2$, $m_3$, $\cdots$, $m_m$ 이라고 하자. 이 때의 match score candidate $C_0$ 를 다음과 같이 정해보자. (이걸 잘 디자인해야 원하는 match ordering 을 얻을 수 있다.)
- 첫 match 위치가 string 처음에 더 가까울수록 점수를 더 주자.
- Case-sensitive match 의 경우 점수를 더 주자. (영어의 경우)
- 개인적으로는 자동 한영변환 match 도 구현해놨는데, 한영-sensitive match 의 경우 점수를 더 주는 것도 생각해볼 수 있음.
- Word 첫번째에 match 될 경우 점수를 더 주자. ("[Physics/Math]--Physics" 같은걸 찾고자 할 때, "pmp" 식으로 많이 칠테니까. "hah" 가 아니라.)
- 한글의 경우 초성에 match 되었을 경우에 점수를 더 주자. (보통 종성 이용해서 검색하지는 않을테니까.)
- 우선 string $S$ 에 pattern $P$ 가 모두 들어 있는지 확인. 이 과정은 우선 $S$ 를 첫부분부터 scanning 하면서 $p_1$ 이 있는지 확인하고, 그 이후위치부터 $p_2$ 가 있는지 확인, 다시 그 이후부터 $p_3$ 이 있는지 확인하는 식으로 모든 $p_i$ 를 확인하는 식으로 이루어진다.
- match 위치들 $m_1, m_2, \cdots, m_m$ 들이 저장되어 있을텐데, 이건 첫 match 들 위치일테니 $m_m$ 위치 이후에 $p_m$ match 가 있는지 확인하고 이때의 match score 가 이전 max match score 값보다 큰지 확인하는 식으로 비교. (Match 사이의 distance 로만 match score 가 결정된다면 $m-1$ 부터 sub-routine 을 시작해도 됨. $m_m$ 을 더 키워봤자 이전보다 낮은 match score 가 나올게 뻔하니까.)
- $m_m$ 이후 $p_m$ match 가 더이상 찾아지지 않는다면, $m_{m-1}$ 위치를 하나 증가시키면서 pattern $p_{m-1}p_{m}$ 을 찾고 이떄의 match score 를 이전의 max match score 와 비교. Sub-routine 으로 바뀐 $m_m$ 이후의 $p_m$ 도 또다시 돌려봐야 함.
- 같은 방식으로 $m_1$ 을 하나 증가시켜서 $p_1 p_2 \cdots p_m$ 을 찾고, sub-routine 까지 다 돌려보면서 max match score 를 찾고 그때의 $m_1, m_2, \cdots, m_m$ 를 기억해 놓으면 됨. findNextMatchFromIndices 로 match 가 없을때까지 계속 찾으면 되는듯. (재귀함수 쓰는게 아닌듯.)
굳이 모든 가능한 match 들을 찾고 그때의 match score 를 다 비교하면서 maximum match score 를 찾을 필요는 없을거 같기도... 그냥 적당히 비교하고 "대충의 maximum match score 는 이정도 값이다."란 것만 뽑아내고 보여줘도 이게 사용자 입장에서 심하게 거슬리지만 않는다면, 굳이 느리게 동작할거 없이 빠르게 대충의 결과를 알려주는게 사용자 경험에는 더 좋을것도 같음. 즉, $m-1$ 부터 sub-routine 을 시작해서 시간을 조금이라도 줄여봅시다? (어느정도에서 협상을 해야할지 헷갈리네. 엄청 느렸던건 뭐 때문이었을까나?)
## Open source
### 기본 variables
```[.linenums.scrollable]
$window=$(window);
$out_focus=$("#out-focus");
$fuzzy_search_container=$("#fuzzy-search-container");
$fuzzy_search=$("#fuzzy-search");
$fuzzy_search_list=$("#fuzzy-search-list");
```/
### 한글의 초성/중성/종성 구분 및 한영/영한 자동변환
한글의 초성/중성/종성을 구분 및 한영/영한 자동변환해서도 동작하게 만들려고 만든 mapping.
```[.linenums.scrollable]
////////////////////////////////////////////////////
// Hangul (Korean) split and map to English
// KE : Korean Expanded
////////////////////////////////////////////////////
m.jamoKE = ["ㄱ", "ㄱㄱ", "ㄱㅅ", "ㄴ", "ㄴㅈ", "ㄴㅎ", "ㄷ", "ㄷㄷ", "ㄹ", "ㄹㄱ", "ㄹㅁ", "ㄹㅂ", "ㄹㅅ", "ㄹㅌ", "ㄹㅍ", "ㄹㅎ", "ㅁ", "ㅂ", "ㅂㅂ", "ㅂㅅ", "ㅅ", "ㅅㅅ", "ㅇ", "ㅈ", "ㅈㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ", "ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅗㅏ", "ㅗㅐ", "ㅗㅣ", "ㅛ", "ㅜ", "ㅜㅓ", "ㅜㅔ", "ㅜㅣ", "ㅠ", "ㅡ", "ㅡㅣ", "ㅣ"];
m.jamo = ["ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄸ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅃ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ", "ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"];
m.mapKE = {"q":"ㅂ", "Q":"ㅃ", "w":"ㅈ", "W":"ㅉ", "e":"ㄷ", "E":"ㄸ", "r":"ㄱ", "R":"ㄲ", "t":"ㅅ", "T":"ㅆ", "y":"ㅛ", "Y":"ㅛ", "u":"ㅕ", "U":"ㅕ", "i":"ㅑ", "I":"ㅑ", "o":"ㅐ", "O":"ㅒ", "p":"ㅔ", "P":"ㅖ", "a":"ㅁ", "A":"ㅁ", "s":"ㄴ", "S":"ㄴ", "d":"ㅇ", "D":"ㅇ", "f":"ㄹ", "F":"ㄹ", "g":"ㅎ", "G":"ㅎ", "h":"ㅗ", "H":"ㅗ", "j":"ㅓ", "J":"ㅓ", "k":"ㅏ", "K":"ㅏ", "l":"ㅣ", "L":"ㅣ", "z":"ㅋ", "Z":"ㅋ", "x":"ㅌ", "X":"ㅌ", "c":"ㅊ", "C":"ㅊ", "v":"ㅍ", "V":"ㅍ", "b":"ㅠ", "B":"ㅠ", "n":"ㅜ", "N":"ㅜ", "m":"ㅡ", "M":"ㅡ"};
for (let p in m.mapKE) {
m.mapKE[m.mapKE[p]]=p;
}
m.rChoKE = ["ㄱ", "ㄱㄱ", "ㄴ", "ㄷ", "ㄷㄷ", "ㄹ", "ㅁ", "ㅂ", "ㅂㅂ", "ㅅ", "ㅅㅅ", "ㅇ", "ㅈ", "ㅈㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];
m.rCho = ["ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];
m.rJungKE = ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅗㅏ", "ㅗㅐ", "ㅗㅣ", "ㅛ", "ㅜ", "ㅜㅓ", "ㅜㅔ", "ㅜㅣ", "ㅠ", "ㅡ", "ㅡㅣ", "ㅣ"];
m.rJung = ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"];
m.rJongKE = ["", "ㄱ", "ㄱㄱ", "ㄱㅅ", "ㄴ", "ㄴㅈ", "ㄴㅎ", "ㄷ", "ㄹ", "ㄹㄱ", "ㄹㅁ", "ㄹㅂ", "ㄹㅅ", "ㄹㅌ", "ㄹㅍ", "ㄹㅎ", "ㅁ", "ㅂ", "ㅂㅅ", "ㅅ", "ㅅㅅ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];
m.rJong = ["", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];
m.splitHangul=function(str) {
let res=[];
res.originalStr=str;
res.splitted3="";
res.splitted="";
let cho, jung, jong;
for (let i=0;i<str.length;i++) {
let c=str.charAt(i)
let n=str.charCodeAt(i);
if (n>=0x3131&&n<=0x3163) { // 자음 'ㄱ', 'ㄲ', 'ㄳ' 만 있는경우. [Ex] 'ㄲ'=>'ㄱㄱ', 'ㄳ'=>'ㄱㅅ' 등.
n-=0x3131;
res[i]={"char":c, "splitted3":c, "splitted":m.jamoKE[n]};
} else if (n>=0xAC00&&n<=0xD7A3) { // 한글일 경우 처리.
n-=0xAC00;
jong=n%28;
jung=( (n-jong)/28 )%21;
cho=( ((n-jong)/28)-jung )/21;
res[i]={"char":c
, "splitted3":m.rCho[cho]+m.rJung[jung]+m.rJong[jong]
, "splitted":m.rChoKE[cho]+m.rJungKE[jung]+m.rJongKE[jong]};
// splitted3 는 초/중/종 으로만 나눈거고, splitted 는 초/종 도 완전히 펼쳐진 형태로.
} else {
res[i]={"char":c, "splitted3":c, "splitted":c};
}
res.splitted3+=res[i].splitted3; // splitted3 끼리 이어놓은 string.
res.splitted+=res[i].splitted; // splitted 끼리 이어놓은 string.
}
return res;
};
```/
Function m.splitHangul
은 한글일 경우 초/중/종 및 자음끼리 맡붙어 있는것들도 처리해주는 함수.
### Fuzzy search 할 list 들 모으기.
기본 variables.
```[.linenums.scrollable]
m.fs=[];
m.fs[0]=[];
m.fs[0]["pattern"]=m.splitHangul("$!@#");
m.fs.fullList=[];
let $list=$(".docuK li");
for (let i=0;i<$list.length;i++) {
let $listI=$list.eq(i);
let $sec=$listI.parents(".docuK>.sec");
let txt="";
let html="";
if ($sec.exists()) {
let cat=$sec.find("h2:first-child .head-txt").text();
let $subsec=$listI.parents(".subsec");
if ($subsec.exists()) {
cat+="\n -- "+$subsec.find("h3:first-child .head-txt").text();
let $subsubsec=$listI.parents(".subsubsec");
if ($subsubsec.exists()) {
cat+="\n -- "+$subsubsec.find("h4:first-child .head-txt").text();
}
}
txt=cat.replace(/\n /g,"").replace(/\n /g,"")+"\n";
html='<div class="cat">'+cat.replace(/\n/g, "<br>")+'</div>';
}
txt+="* "+$listI.text();
html+='<div class="li">* '+$listI.html()+'</div>';
m.fs.fullList[i]={i:i, "txt":m.splitHangul(txt), "html":html, "$listI":$listI};
}
```/
### Trivial functions.
```[.linenums.scrollable.lang-js]
RegExp.quote=function(str) {
return str.replace(/[.?*+^$[\]\\{}()|-]/g, "\\$&").replace(/\s/g, "[\\s\\S]");
};
m.arrayRegExs=function(str) {
let res=[];
for (let i=0;i>str.length;i++) {
let c=str.charAt(i);
let mapKE=m.mapKE[c];
if (mapKE) {
res.push( new RegExp("["+c+mapKE+"]", "ig") );
} else {
res.push( new RegExp(RegExp.quote(c), "ig") );
}
}
return res;
};
```/
### Match 된 곳 highlight 시키는 function.
```[.linenums.scrollable.lang-js]
m.highlightStrFromIndices=function(strSplitted, indices) {
let res="";
for (let i=0, j=1, k=0, p1=0, p2=0;j<=indices.length;i=j,j++) {
while (j<indices.length&&indices[j-1].end===indices[j].start) {
j++;
}
for (;k<strSplitted.length;k++) {
p1=p2;
p2=p1+strSplitted[k]["splitted"].length;
if (p2<=indices[i].start) {
strSplitted[k]["matched"]=false;
} else if (p1<indices[j-1].end) {
strSplitted[k]["matched"]=true;
} else {
if (j===indices.length) {
for (;k<strSplitted.length;k++) {
strSplitted[k]["matched"]=false;
}
}
p2=p1;
break;
}
}
}
for (let i=0;i<strSplitted.length;) {
if (strSplitted[i]["matched"]) {
res+='<span class="bold">';
while (i<strSplitted.length&&strSplitted[i]["matched"]) {
res+=m.escapeHTML(strSplitted[i]["char"]);
i++;
}
res+='</span>';
} else {
while (i<strSplitted.length&&!strSplitted[i]["matched"]) {
res+=m.escapeHTML(strSplitted[i]["char"]);
i++;
}
}
}
return res;
};
```/
### Match Score from Indices
기본적으로 fuzzySearch(ptnSH (pattern splitHanguled), fs (fuzzy search result))
```[.linenums.scrollable.lang-js]
m.fuzzySearch=function(ptnSH, fs) {
let list=[];
if (ptnSH.splitted.indexOf(fs[0].ptnSH.splitted)===0) {
fs[1]=fs[0];
} else if (fs[1] && ptnSH.splitted.indexOf(fs[1].ptnSH.splitted)===0) {
if (ptnSH.splitted===fs[1].ptnSH.splitted) {
return fs[1];
}
} else {
fs[1]=null;
}
if (fs[1]&&fs[1].sorted) {
let sorted=fs[1].sorted;
for (let i=0;i<sorted.length;i++) {
list.push(fs.fullList[fs[1][sorted[i]].i]);
}
} else {
list=fs.fullList;
}
let res=[];
res.ptnSH=ptnSH;
let regExs=m.arrayRegExs(ptnSH.splitted);
for (let i=0;i<list.length;i++) {
if (regExs.length>0) {
let txt=list[i].txt;
let txtS=txt.splitted;
regExs[0].lastIndex=0;
let exec=regExs[0].exec(txtS);
let matched=(exec!==null);
let indices=[];
if (matched) {
indices[0]={start:exec.index, end:regExs[0].lastIndex};
}
for (let j=1;matched&&(j<regExs.length);j++) {
regExs[j].lastIndex=regExs[j-1].lastIndex;
exec=regExs[j].exec(txtS);
matched=(exec!==null);
if (matched) {
indices[j]={start:exec.index, end:regExs[j].lastIndex};
}
}
if (matched) {
let maxMatchScore=m.matchScoreFromIndices(txt, ptnSH, indices);
let indicesMMS=[]; // indices of max match score
for (let p=0;p<indices.length;p++) {
indicesMMS[p]=indices[p]; // hard copy of indices
}
for (let k=indices.length-2;k>=0;) {
regExs[k].lastIndex=indices[k].start+1;
exec=regExs[k].exec(txtS);
matched=(exec!==null);
if (matched) {
indices[k]={start:exec.index, end:regExs[k].lastIndex};
}
for (let j=k+1;matched&&(j<regExs.length);j++) {
regExs[j].lastIndex=regExs[j-1].lastIndex;
exec=regExs[j].exec(txtS);
matched=(exec!==null);
if (matched) {
indices[j]={start:exec.index, end:regExs[j].lastIndex};
}
}
if (matched) {
let matchScore=m.matchScoreFromIndices(txt, ptnSH, indices);
if (matchScore>maxMatchScore) {
maxMatchScore=matchScore;
for (let p=0;p<indices.length;p++) {
indicesMMS[p]=indices[p]; // hard copy of indices
}
}
k=indices.length-2;
} else {
k--;
}
}
res.push({i:list[i].i, maxMatchScore:maxMatchScore, highlight:m.highlightStrFromIndices(txt, indicesMMS)});
}
} else {
res.push({i:list[i].i, maxMatchScore:0});
}}
// sorting...
let sorted=res.sorted=[];
for (let i=0;i<res.length;i++) {
sorted[i]=i;
}
for (let i=1;i<sorted.length;i++) {
let temp=sorted[i];
let j=i;
for (;(j>0) && (res[sorted[j-1]].maxMatchScore<res[temp].maxMatchScore);j--)
sorted[j]=sorted[j-1];
sorted[j]=temp;
}
return res;
};
```/
### Special keys (ESC, up, down) :: $fuzzy_search.on("keydown")
```[.linenums.scrollable.lang-js]
$fuzzy_search.on("keydown", function(e) {
e.stopPropagation();
switch (e.keyCode) {
case 27: // ESC = 27
e.preventDefault();
$out_focus.focus();
$fuzzy_search_container.hide();
break;
case 38: // up = 38
case 40: // down = 40
e.preventDefault();
let $fsl=$fuzzy_search_list;
let $lis=$fsl.find(".list-item");
let $liSelected=$fsl.find(".list-item.selected").eq(0);
let $liTo=null;
if ($liSelected.exists()) {
if (e.keyCode===38) {
$liTo=$liSelected.prev();
} else {
$liTo=$liSelected.next();
}
if ($liTo.exists()) {
$liTo.eq(0).trigger("click");
if ($liTo.offset().top<$fsl.offset().top+2) { // $liTo at upside of scroll.
$fsl.scrollTop($fsl.scrollTop()+$liTo.offset().top-$fsl.offset().top-2);
} else if ($liTo.offset().top+$liTo.outerHeight()>$fsl.offset().top+$fsl.height()+2) { // $liTo at downside of scroll.
$fsl.scrollTop($fsl.scrollTop()+$liTo.offset().top+$liTo.outerHeight()-$fsl.offset().top-$fsl.height()-2);
}
}
} else {
if ($lis.exists()) {
if (e.keyCode===38) {
$liTo=$lis.last();
$fsl.scrollTop($fsl[0].scrollHeight);
} else {
$liTo=$lis.first();
$fsl.scrollTop(0);
}
$liTo.eq(0).trigger("click");
}
}
break;
}
});
```/
### Do Fuzzy Search
Delayed (Lazy) Loading 이랑 비슷하게 구현. 이렇게 해야만 속도가 빨라지고, UX 가 좋아지는듯도? 아닌가? clearTimeout
, setTimeout
만 써서도 만들어 봤었는데... 너무 느리길래 이렇게 바꾼거긴 한데...
```[.linenums.scrollable.lang-js]
m.doFS=function() {
let fsPtnSH=m.splitHangul($fuzzy_search.text());
if (m.fs[0].ptnSH.splitted!==fsPtnSH.splitted) {
m.fuzzySearch(fsPtnSH, m.fs);
let res=m.fs[0];
let sorted=res.sorted;
let str="";
for (let i=0;i<sorted.length;i++) {
let k=res[sorted[i]].i;
str+='<div class="list-item" onclick="m.gotoLi(event,this,'+k+')">'
+m.fs.fullList[k]["html"]
+((res[sorted[i]]["highlight"]!==undefined)?
'<div class="highlighted">'+'<span class="maxMatchScore">'+res[sorted[i]].maxMatchScore+'</span> :: '+res[sorted[i]]["highlight"]+'</div>':'')
+'</div>';
}
$fuzzy_search_list.html(str);
$fuzzy_search_list.scrollTop(0);
m.$fsLis=$fuzzy_search_list.find(".list-item");
}
};
m.previous=Date.now();
m.fsOn=function() {
let now=Date.now();
let passed=now-m.previous;
if (passed>m.wait) {
m.doFS();
m.previous=now;
} else {
$fuzzy_search.off("input.fs keyup.fs cut.fs paste.fs");
setTimeout(function() {
m.doFS();
m.previous=Date.now();
$fuzzy_search.on("input.fs keyup.fs cut.fs paste.fs", m.fsOn);
}, m.wait*1.1-passed);
}
};
$fuzzy_search.on("input.fs keyup.fs cut.fs paste.fs", m.fsOn);
$fuzzy_search.trigger("keyup");
```/
### Result (JSON)
JSON 형태의 Result
```[.linenums.lang-java]
// Fuzzy search 결과를 저장할 변수들.
m.fs=[];
m.fs[0]=[];
m.fs[0]["ptnSH"]=m.splitHangul("$!@#");
// m.splitHangul 결과는 {"char":"꺆", "splitted3":"ㄲㅑㄲ", "splitted":"ㄱㄱㅑㄱㄱ"};
// Fuzzy search 할 string list 가 다음과 같이 존재함.
m.fs.fullList=[];
m.fs.fullList[i]={i:i, "txt":m.splitHangul(txt), "html":html, "$listI":$listI};
// m.splitHangul 결과는 {"char":"꺆", "splitted3":"ㄲㅑㄲ", "splitted":"ㄱㄱㅑㄱㄱ"};
// Fuzzy search 결과는 다음과 같이 저장됨.
m.fs=[];
m.fs[0]=[]; // 이번 fuzzy search 결과.
m.fs[1]=[]; // 1번전의 fuzzy search 결과.
// 스마트폰에서 한글 입력시 "ㄱ,ㄲ" 가 바뀌면서 입력되는 형태도 있으니... 한글자만 지우고 다시 입력하는 경우도 빈번하고...
m.fs[0]["ptnSH"]=m.splitHangul("김");
m.fs[1]["ptnSH"]=m.splitHangul("기");
// "김" 검색했다가 "깁" 같은걸로 바꿔서 검색하는 경우 m.fs[1] 을 참고해서 후속 검색.
// "김ㅂ" 을 검색한다면 m.fs[0] 를 참고해서 후속 검색하는거고. m.fs[1]=m.fs[0];
// 아무튼 on, off, setTimeout 등을 잘 이용해서 구현. ptnSH.splitted.indexOf(fs[0].ptnSH.splitted)===0
같은 query 이용해서 효율적으로 동작하도록 해놓음.
// 결과는 index 들 모아놓고, 각각의 max match score 저장해놓고, 그때의 match 위치들 indices 저장 (highlighted string html만?) 해놓고, sorted index (DESC maxMatchScore) 도 저장.
m.fs[0][0]={i:i, maxMatchScore:xxx, highlight:xxxx};
fsResult[0]["sorted"]=[sorted i's]; // sorted indices 를 따로 저장해놓을까나?
m.fs[0]=[];
m.fs[0].ptnSH=m.splitHangul(pattern);
m.fs[0][0]={i:list[i].i, maxMatchScore:maxMatchScore, highlight:m.highlightStrFromIndices(txt, indicesMMS)});
m.fs[0][1]={i:list[i].i, maxMatchScore:maxMatchScore, highlight:m.highlightStrFromIndices(txt, indicesMMS)}
.......
m.fs[0].sorted=[9,2,4,1,7,3,5,8,6];
// m.fs.fullList 에서 찾으려면, m.fs.fullList[fs[0][sorted[i]].i]
```/
## prefix 명령어?
Sublime text 에서는 @
, #
, :
등은 prefix 를 이용해서 조금 다른 방식으로 fuzzy search 가 동작하도록 해놨는데, 나도 이런게 좀 필요하긴 할듯?
여러 단어를 입력받고 그 단어들간의 순서는 중요하지 않지만 더 겹치는게 많은 string 을 뽑아낸다던지 하는 방식으로?
특정 reco 에 어울리는 category 를 찾는다고 할 때, 이 reco 의 제목에서 자동으로 어울리는 category 를 match 시켜준다던지 할때 유용할거 같은데... 아님 글쓴이가 내 글은 이런이런 단어들이 keyword 라고 설정해놓고 reco 버튼을 추가해 놓으면 그 keyword 들 가지고 적절한 category 를 찾아준다던지 할때...
아무튼 여러 형태/방식으로 fuzzy search 를 변형/구체화/세분화 할 수 있으면 좋은점이 많을듯.
## 입력때마나 검색에 해당하는지 check 하기. (바로전 search 내역만 기억해놓기?)
검색해야할 string list 가 그리 크지 않거나, user-specific 한 경우에는 검색어에 대해 미리 indexing 해놓는게 그리 효율좋은 방법은 아닐테니, 그냥 검색어 입력받을때마나 그때그때 match 시켜보고 match score 구하고 정렬하는게 맞는 방법 같음.
핸드폰으로 검색어 입력할때는 하나하나씩 입력하다가 하나씩 지우고 다시쓰는 경우가 빈번하니 바로전 search 결과는 저장해놓는게 좋아보이기도 함.
## Indexing : 검색 결과를 모두 table 에 미리 저장해두기?
이 부분은 아직 정리 덜했음. 아이디어만... 검색해야 하는 string list 가 엄청나게 크다던가, 추가로 여러 사람이 같은 data (string list) 에 대해 검색하는 경우라면 이런 indexing 이 더 좋은방법 같긴 함. User-specific 이라도 꽤나 자주 검색을 사용한다던가 하는 경우도 indexing 이 더 검색효율을 좋게 만들어 줄지도?
무식하게는 category list 의 모든 가능성을 indexing 으로 저장해 놓고 뽑아 쓰는 것일텐데... "abcde" 가 있으면 "a", "b", "c", "d", "e", "ab", "ac", "ad", "ae", "bc", "bd", "be", "cd", "ce", "de", "abc", "abd", "abe", "acd", ...... 이런식으로만 쳐도 "abcde"가 뜨는식.
"abaaabbc" 도 있다면 "abc" 를 치면 ["abcde", "abaaabbc"] 가 다 떠야함. (정렬순서는 match score 따라서.) 뭐 실제 어떻게 동작하는지, 얼마나 편하지는 쓰면서 느껴봐야 제대로 체감할듯.
무식하게 위와 같이 1차원 배열 식으로 할수는 없을테고 (짜기는 쉽겠지만 메모리를 많이 잡아먹을듯? 아닌가? 그냥 이렇게 무식하게 해서 Hash table 에 넣어놓는게 성능면에서 가장 좋을듯도 하고...), 조금 더 체계적으로는 chainning 이나 recursive 쓰면 될거 같은데? 더 복잡해지기만 하나?
```[.linenums.scrollable]
가장 무식하게 Hash table 만들기.
// "abcde" 가 list 에 있다면,
e: "e" -> []
de: "d" -> [e]
cde: "c" -> [de, e]
bcde: "b" -> [cde, de, e]
abcde: "a" -> [bcde, cde, de, e]
// 와 같은 JSON 비스무레 한 것이 있으면 될듯? JSON 식의 tree 구조를 생각해 봤는데 안될듯함 ㅡㅡ;;;;, 제대로 동작하게 하려면 구조가 너무 복잡해지거나. (복잡하면 그냥 무대뽀식의 hash table 보다 나을게 없음.) 원하는대로 동작되는 복잡한 구조도 당장 생각이 안남.
// 위를 조금 더 풀어서 쓰자면,
catList["e"].push("abcde"); // Value 가 저장되나? Reference (string address) 만 저장되는게 좋은데...
catList["d"].push("abcde");
catList["de"].push("abcde");
catList["c"].push("abcde");
catList["cd"].push("abcde");
catList["cde"].push("abcde");
catList["ce"].push("abcde");
catList["b"].push("abcde");
catList["bc"].push("abcde");
catList["bcd"].push("abcde");
catList["bcde"].push("abcde");
catList["bce"].push("abcde");
catList["bd"].push("abcde");
catList["bde"].push("abcde");
catList["be"].push("abcde");
catList["a"].push("abcde");
catList["ab"].push("abcde");
......
......
catList["ae"].push("abcde");
// 와 같은 Hash table 이 만들어 지는 것.
"bd" 가 입력되었다면, catList["bd"] 에 저장되어 있는 list 들을 뽑아냄.
// "ze" 가 추가로 list 에 들어간다면,
e: "e" -> []
ze: "z" -> [e]
// 위를 더 풀어서 쓰자면,
catList["e"].push("ze");
catList["z"].push("ze");
catList["ze"].push("ze");
// 와 같이 됨.
1
1 + (1)
1 + (1+(1)) + (1)
1 + (1+(1+(1))+(1)) + (1+(1)) + (1)
1 + (1+(1+(1+(1))+(1))+(1+(1))+(1)) + (1+(1+(1))+(1)) + (1+(1)) + (1)
```/
;
구분자로 여러개 들어갈 수 있어서 이런것도 처리해줘야 할텐데... 완성된 cat 은 boxed 로 묶어서 보기 쉽게 만들어도 줘야 하겠고... x 버튼 (클릭으로 지우기) 같은 것도 생각해봐야겠고... 왜이리 많니? ;
, --
관련해서 format 도 잘 갖춰져 있는지 javascript 에서 test 해주는게 좋을듯.
적절한 위치에 list up 이 보여야 하고. 너무 많으면 scroll, 적을땐 그 화면에 맞춰서. (css: max-height) 설정으로 조정해줘야 할듯? Bubble refer 때 썼던 position 응용하고. 확실히 sublime 이 잘되어 있어서 잘 참조해야 할듯.
정렬순서도 중요해 보이긴 함. (지금은 그냥 catList 순서대로만 보여줌.) 이건 UX 를 어떻게 디자인 할까랑도 연결될듯? 첫 char 위치가 적은 순서대로 보여준다던지...
## RRA
- Sublime text
- Wiki - Approximate string matching
- crossplatform.net - Sublime Text Ctrl+P Like Fuzzy Matching In Few Lines of Python, 2013-06-16, by Raahul Seshadri
- blog.amjith.com :: FuzzyFinder - in 10 lines of Python, 2015-06-23
- juggernaut.tistory.com :: 퍼지 문자열 검색 (Fuzzy string search), 2012-12-27, by 임은천
원문 : Нечёткий поиск в тексте и словаре, 2011-03-09, by Никита Сметанин (러시아어인듯)
영어번역 : NIKITA'S BLOG :: Fuzzy string search, 2011-03-24
// 다양한 형태/방법의 fuzzy search 에 관해 설명.
728x90
반응형
'[Recoeve.net]' 카테고리의 다른 글
안산시 단원구 고잔동 이마트24 안산천남로점 유통기간 임박상품 무료나눔+(프린팅박스|PrintingBox, Recoeve.net 홍보) (5) | 2023.11.09 |
---|---|
스타트업 경영해 나갈때 참조할 만한 글들 (0) | 2023.10.29 |
개인화 된 추천 시스템 (Personalized recommendation system) (3) | 2023.10.25 |
Recoeve.net page loading and scrollTop Error (0) | 2023.05.30 |
Exception: java.sql.SQLException: java.time.LocalDateTime to SQL type (0) | 2023.05.30 |
음식점 할인정보 정리 (App Surprise) (0) | 2022.12.30 |
Programmer/Developer's Portfolio (개발자/프로그래머의 포트폴리오) (2) | 2022.12.28 |