Skip to content

Commit

Permalink
docs: 포스트 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
doputer committed Feb 11, 2024
1 parent 3b81af3 commit 97c3ac2
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 0 deletions.
Binary file added contents/2022/12/19/0/0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added contents/2022/12/19/0/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added contents/2022/12/19/0/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions contents/2022/12/19/0/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
emoji: '🎯'
title: '검색어 강조 알고리즘 개발기'
description: '이걸 검색한게 맞으시죠'
tags:
- article
- algorithm
- search
date: 2022-12-19
---

## 기존 Knoticle의 검색 결과

서비스에서 "페이지" 라는 키워드로 검색했을 때 기존에는 다음과 같이 검색된다.

![](0.png)

제목과 내용에 "페이지" 라는 검색어가 포함된 글들이 나타나고 있다는 걸 확인할 수 있다.

그런데 사용자 입장에서는 원하는 검색 결과가 나타났는지 알기 힘들다는 생각이 들었다. 사용자가 검색한 검색어가 글의 어떤 맥락에 들어있는지 알게 해준다면, 원하는 검색 결과를 빠르게 선택할 수 있을 것이다.

![](1.png)

위와 같이 검색 결과에 검색어가 강조되는 기능을 구현한 과정을 소개하려고 한다.

## 검색어 강조하기

Knoticle의 글(article)의 내용(content)은 다음과 같은 형식으로 저장되어있다.

> 국토와 자원은 국가의 보호를 받으며, 국가는 그 균형있는 개발과 이용을 위하여 필요한 계획을 수립한다. 모든 국민은 통신의 비밀을 침해받지 아니한다. 감사원은 세입·세출의 결산을 매년 검사하여 대통령과 차년도국회에 그 결과를 보고하여야 한다. 대한민국은 민주공화국이다. 국가는 재해를 예방하고 그 위험으로부터 국민을 보호하기 위하여 노력하여야 한다.
이때 "국민" 과 "예방" 이라는 검색어로 검색한 경우에 어떤 식으로 해당 단어를 강조해줄 수 있을까?

우선 문자열을 앞에서 부터 읽으면서 "국민" 과 "예방" 중에 먼저 등장하는 단어를 찾아야한다.

> ...
>
> 모든 **국민**은 통신의 비밀을 침해받지 아니한다.
>
> ...
코드로는 다음과 같이 작성할 수 있다.

```ts
const getFirstKeyword = (text: string, keywords: string[]) => {
const keywordMap = new Map<number, string>();

keywords.forEach((keyword) => {
const index = text.toLowerCase().indexOf(keyword.toLowerCase());

if (index !== -1) keywordMap.set(index, text.slice(index, index + keyword.length));
});

if (keywordMap.size === 0) return { keyword: '', index: -1, validKeywords: [] };

const firstKeywordIndex = Math.min(...Array.from(keywordMap.keys()));
const firstKeyword = keywordMap.get(firstKeywordIndex);

return {
keyword: firstKeyword || '',
index: firstKeywordIndex,
validKeywords: Array.from(new Set(keywordMap.values())),
};
};
```

검색어들을 순회하면서 내용에서 가장 먼저 등장하는 인덱스를 구한다. 그 후에 가장 낮은 인덱스를 가진 단어(= 가장 앞에 등장하는 단어)를 찾는다.

이때 영어 검색의 경우 대소문자 구분 없이 찾기 위해서 `.toLowerCase()`를 이용한다. 또한 한번 찾지 못한 검색어는 같은 문장에서 다시 등장하지 않기 때문에 이를 필터링해주기 위해 `validKeywords`에 유효한 검색어만 필터링해서 같이 반환해준다.

가장 먼저 등장하는 단어를 찾았다면 다음과 같이 단어를 기준으로 문장을 세 등분(단어 앞 문장, 단어, 단어 뒷 문장)으로 나눈 후 사이에 `<b> 태그`를 넣어서 강조해줄 수 있다.

> ...
>
> 모든 \<b\>국민\</b\> 은 통신의 비밀을 침해받지 아니한다.
>
> ...
단어 하나를 강조했다면 단어 뒷 문장은 동일한 방법으로 강조처리를 해주면 된다.

1. 가장 먼저 등장하는 검색어 찾기
2. 검색어를 기준으로 검색어 앞 문장, 검색어, 검색어 뒷 문장으로 나누기
3. 나눠진 문장 사이에 \<b\> 태그 추가하기
4. 검색어 뒷 문장에서 가장 먼저 등장하는 검색어 찾기
5. 반복

코드로는 다음과 같이 작성할 수 있다.

```ts
export const highlightKeyword = (text: string, keywords: string[]): React.ReactNode => {
const { keyword, index, validKeywords } = getFirstKeyword(text, keywords);

if (index === -1) return text;

const endIndex = index + keyword.length;

return (
<>
{text.slice(0, index)}
<b>{keyword}</b>
{highlightKeyword(text.slice(endIndex), validKeywords)}
</>
);
};
```

## 강조한 검색어가 보이지 않는 문제

검색어를 강조할 수 있는 로직은 작성했는데 일부 검색 결과에서 문제가 있었다.

검색 결과의 제한된 뷰포트로 인해 강조한 검색어가 문장 뒷 부분에 등장하는 경우 검색 결과에서 보이지 않게 되었다.

![](2.png)

검색어가 포함된 문장이 보이게 해준다면 사용자는 자신의 검색어가 글의 어떤 맥락에서 등장하는지 쉽게 찾을 수 있게 된다.

이를 해결하기 위해서는 최초 검색어의 앞 부분을 제거해주면 된다.

> ~~국토와 자원은 국가의 보호를 받으며, 국가는 그 균형있는 개발과 이용을 위하여 필요한 계획을 수립한다.~~
>
> ~~모든~~ **국민**은 통신의 비밀을 침해받지 아니한다.
>
> ...
다만 위와 같이 검색어 바로 앞쪽부터 지우게 되면 검색어가 포함된 문장도 지워지기 때문에 사용자가 맥락을 파악하기 힘들어진다.

검색어가 포함된 문장은 살리되 그 앞쪽은 모두 지우기 위해선 줄바꿈 문자를 이용하면 된다. 검색어가 포함된 문장과 가장 가까이 있는 줄바꿈 문자를 찾아서 그 앞쪽을 지우면 앞서 말한 문제들을 해결할 수 있게 된다.

> ~~국토와 자원은 국가의 보호를 받으며, 국가는 그 균형있는 개발과 이용을 위하여 필요한 계획을 수립한다.~~**\n**
>
> 모든 **국민**은 통신의 비밀을 침해받지 아니한다.
>
> ...
코드로는 다음과 같이 작성할 수 있다.

환경에 따라 다르지만 뷰포트에는 최대 400자 정도가 들어올 수 있기 때문에 불필요한 뒷 부분도 함께 제거해줬다.

```ts
export const getTextAfterLastNewLine = (text: string, keywords: string[]) => {
const { index } = getFirstKeyword(text, keywords);

const newLineIndex = text.slice(0, index).lastIndexOf('\n');

return newLineIndex === -1 ? text.slice(0, 400) : text.slice(newLineIndex, newLineIndex + 400);
};
```

0 comments on commit 97c3ac2

Please sign in to comment.