포스트

[DB] 데이터베이스 <2 : Reids>

0. 왜 갑자기 Redis?

원래 로그인 기능, 게시판 기능처럼 스탠다드한 기능을 구현하기 위해 MySQL, MongoBD를 먼저 공부해보려 했으나 긴급하게 BD의 검색어 자동완성 기능을 구현할 일이 있어서 먼저 Redis에 대해 알아보려고 한다.

1. 그래서 Redis를 고른 이유

검색어 자동완성 기능은 사용자와 직접 상호작용하는 부분이기 때문에 무엇보다도 사용자 경험이 중요하다.

그렇기에 검색창에 단어를 입력했을때, 빠른 속도로 사용자가 입력한 단어와 연관된 단어를 추천해주는게 중요하다. 한글자 한글자 입력할 때마다 다른 추천검색어가 나올 수 있어야 한다. 이런 관점에서 앞서 알아본 NoSQL에 속하는 관리 시스템 중에서 고르게 됐다.

그 중 Redis 는 Key - Value 타입의 저장소이고, 다양한 자료구조와 빠른 속도에서 강점이 있다. 특히 자료구조 중 Sorted Sets는 {검색어 : 점수} 형태로 데이터를 저장하며 이때 점수를 기준으로 정렬한다면 빈출 자동완성에 도움이 되지 않을까 생각했다.

레디스 자료구조 종류

여담 Redis가 빠를 수 있는 이유

Redis는 다른 db와 다르게 In-memory 방식이다. redis는 데이터를 읽고 쓰는 과정을 모두 메모리 위에서 해결하기 때문에 하드디스크를 이용하는 방식보다 빠르다.

대신 램은 휘발성 기억장치인지라, 서버가 꺼지는 순간 데이터가 전부 날아가게 된다. 그렇기에 날아가도 별로 타격이 없지만, 빠르게 데이터를 입출력하고 싶을때 사용한다.

2. 자동완성을 어떻게 구현할지?

먼저 다른 블로그에 redis를 사용해 자동완성을 구현하신 분이 있어서 참고해봤다.

출처 : https://tlatmsrud.tistory.com/106

2.0 그 전에 Redis 명령어 알아보기

  1. ZADD

    → ZADD key score value

    데이터셋에 데이터를 점수와 함께 추가하는 명령어

    ex) ZADD database 1 젤다

  2. ZRANGE

    → ZRANGE key startindex endindex

    주어진 범위 내에서 데이터셋을 반환한다. 이때 점수가 작은것부터 반환하며, 점수가 동일할 경우 사전순으로 조회된다.

    ex) ZRANGE database 0 - 1

  3. ZRANK

    → ZRANK key value

    정렬된 기준이 오름차순일 경우, 검색한 데이터의 index를 반환한다.

    → ZRANK database 젤다

    ← 0

2.1 자동완성 알고리즘 살펴보기

사용자가 검색어를 입력했을 시 단어(이하 Prefix)를 포함하고있는 완성된 단어를 조회해 출력해야한다.

이때 따로 enter를 누르지 않고 짧은 시간마다 혹은 사용자 입력이 있을 때마다 함수를 실행시키면 될 듯 하다.

그렇다면 Prefix를 포함한 단어를 어떻게 조회할까가 핵심이 된다.

  1. 검색할 단어를 하나씩 끊어서 데이터를 재생산하는 방법

    출처 : https://tlatmsrud.tistory.com/106

    예를 들어 “기억을 걷는 시간”의 경우

    [기, 기억, 기억을, 기억을(공백) … 기억을 걷는 시, 기억을 걷는 시간, 기억을 걷는 시간*]

    으로 만든 후, 검색어가 들어오면 해당 index를 얻는다.

    데이터는 반드시 사전순으로 정렬되어 있기 때문에, index 이후에 특정 범위 내의 단어들 중 suffix (여기에서는 *) 를 포함한 단어만 추출 후 suffix를 지운 후 내보내면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import redis

rd = redis.StrictRedis(host='localhost', port=6379, db=0)

searchword = ""

searchidx = rd.zrank("autocomplete2",searchword)

result_list = rd.zrange("autocomplete2", start = searchidx, end = searchidx+1000)

result_str_list = [member.decode('utf-8') for member in result_list]

def mapper(x):
    """ 값이 검색어로 시작하면서 *를 포함한 단어만 필터링한다."""
    return x.startswith(searchword) and x[-1] == "*"

def delete_star(x):
    return x.replace('*', '')

result_with_star = list(filter(mapper,result_str_list))

final_result = list(map(delete_star,result_with_star))

print(final_result)
  1. ZRANGE를 통해 검색하는 방식. 최소와 최대 범위를 지정해주면 된다.

    참고! : 검색 시, 대괄호 [ 는 값을 포함할 경우, 소괄호 ( 는 값을 제외할 경우에 사용된다.

    출처 : https://velog.io/@grit_munhyeok/겜린더-검색-자동완성-성능을-개선해-보기

    ZRANGE myset “[tt” “[tt\xff” BYLEX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import redis
import pandas as pd

rd = redis.StrictRedis(host='localhost', port=6379, db=0)

df = pd.read_csv('List.csv')

titles = df['title'].tolist()

zadd_data = {title: 0 for title in titles}

rd.zadd('autoComplete', zadd_data) # 데이터 추가 과정

result_bytes = rd.zrangebylex('autoComplete', min='[문자열', max='[문자열\xff')

result_str = [member.decode('utf-8') for member in result_bytes]

print(result_str)

2.2 실행결과

1번 실행결과

꽤나 만족스러웠다. 검색할 글자가 한글자여도 로직 상 검색에 문제될 것이 전혀 없었다.

문제점은 prefix로 시작하지 않는 단어의 경우 검색이 불가능하다는 것

예를 들어 “기억을 걷는 시간” 의 경우,

“기”, “기억”, “기억을” 으로는 검색 가능하고

“걷는” 혹은 “시간” 으로는 검색이 불가능하다.

2번 실행결과

1번과 달리 검색 DB를 따로 만들지 않아도 되는 장점이 있었다.

하지만 문제점이 1번의 경우보다 더 까다로웠다.

예를 들어 “기억을 걷는 시간” 의 경우,

“기억을” 으로는 검색 가능하고

“기”, “기억”, “걷는” 혹은 “시간” 으로는 검색이 불가능하다.

즉 검색 단어의 prefix 이면서 공백 전까지만의 단어로만 검색이 가능했다.

3. 데이터를 추가할 경우엔?

현재 만든 DB는 아예 새로 만든 데이터라서 사전순 정렬이 되어있지만 만약 이미 만들어져있는 DB에 새 데이터를 추가한다면 제대로 정렬이 될지 의문이다. 특히 1번의 경우 아예 잘려진 단어들이 마구잡이로 섞일 가능성도 있어서 이 부분은 추후에 테스트 해볼 계획이다.

자료 참고 :

https://devlog-wjdrbs96.tistory.com/374



이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.