ChatForYou.io 실시간 화상채팅 프로젝트(2) Redis 데이터 검색하기(feat. redisSearch)
1. 시작하면서
이번에도 정말 오랜만에 포스팅을 남기네요. 정말 많은 일이 있었습니다. 변명이라도 하기에는...사실 이번에는 정말 변명거리가 없네요ㅠㅠㅠ그냥 적당히 많이 바쁘기도 했고 정말로 저번주에는 포스팅을 꼭 쓰려고했는데 기존에 사용하던 이미지인 redislabs/redismod 에서 최신 이미지인 redis/redis-stack 로 변경하면서 설정하고 세팅하는데 이상하게 시간이 오래 걸렸습니다ㅠㅠ
사실 redis 뿐만 아니라 토큰발급하고 이걸 적용했던 것에 대한 이야기도 좀 해야하구 이야기할게 참 많은데 일단 일단은!! 오늘은 redisSearch 를 위주로 이야기해보도록 하겠습니다.
2-1. 왜 RedisSearch 야?
chatRoom 에 대한 데이터를 redis 를 저장하는 건 굉장히 좋은 선택이었다. 단순히 새로운 것을 접한다라는 점보다 효율적으로 데이터를 저장하고 가져오는 방법을 익히는게 재미있기도 했고, 이를 통해 성능개선을 이뤄낸 부분도 있고 말이다. 문제는 RDB 에서 데이터를 검색하는 쿼리는 알아도 redis 에서 특정 데이터를 가져오는 검색을 위한 방법은 몰랐다는 점이다. 그래서 보통 redis 와 Elasticsearch 도 함께 사용한다고는 하는데 문제는 내가 Elasticsearch 를 사용하기 위해 서버를 구성할 시간도 자원도 되지 않았다는 점이다.
여기서 생겼던 의문은 그럼 만약 나처럼 실제 환경에서 redis 만 사용하는 사람들은 어떻게 검색하지? 라는 점이었다. 때문에 분명 무엇인가 있을꺼라고 확신하고 있었다. 그렇게 찾게 된 것이 redis 안에서 검색을 도와주는 RedisSearch 라는 모듈이다.
2-2. Redis - redis/redis-stack
Redis Search 는 redis 와 함께 사용하는 일종의 redis 모듈이다. 그리고 당연하다면 당연하게도 redis 에는 단순히 redis Search 이외에도 정말 다양한 모듈이 존재한다. 그리고 일반 redis 와는 다르게 이러한 모듈을 모아둔 redis 이미지가 있다. 바로 redis-stack 이다.
https://hub.docker.com/r/redis/redis-stack
그리고 아래는 redis stack 에서 활성화 할 수 있는 모듈들이다. 역시나 AI, 빅데이터가 대세는 대세인지 redis 도 그에 맞춰 관련된 다양한 모듈이 존재한다. 나의 경우에는 해당 모듈들까지는 필요없기에 RedisSearch 와 RedisJSON 만 활성화해서 사용한다. 이때 search 는 검색을 위한 용도이고, json 은 redis 에 데이터를 JSON 형태로 저장하고 조작할 때 유용하게 사용되기 때문이다.
모듈명 | 기능 | 사용 사례 | 주요 명령어 | 로드 명령어 |
RediSearch | 고성능 텍스트 검색 및 인덱싱 | 전체 텍스트 검색, 필드 기반 검색 | FT.CREATE, FT.SEARCH | --loadmodule /path/to/redisearch.so |
RedisJSON | JSON 데이터 저장 및 쿼리 | 구조화된 데이터, 부분 업데이트 | JSON.SET, JSON.GET | --loadmodule /path/to/rejson.so |
RedisTimeSeries | 시계열 데이터 저장 및 분석 | IoT 데이터, 모니터링 | TS.CREATE, TS.ADD, TS.RANGE | --loadmodule /path/to/redistimeseries.so |
RedisGraph | 그래프 데이터베이스 | 소셜 네트워크, 추천 시스템 | GRAPH.QUERY, GRAPH.EXPLAIN | --loadmodule /path/to/redisgraph.so |
RedisBloom | 확률적 데이터 구조 (블룸 필터 등) | 중복 확인, 통계 수집 | BF.ADD, BF.EXISTS | --loadmodule /path/to/redisbloom.so |
RedisAI | 머신러닝 및 딥러닝 모델 실행 | 실시간 예측, 모델 추론 | AI.MODELSET, AI.MODELRUN | --loadmodule /path/to/redisai.so |
RedisGears | 데이터 처리 파이프라인 및 스크립팅 | 실시간 데이터 처리, ETL 작업 | RG.PYEXECUTE, RG.TRIGGER | --loadmodule /path/to/redisgears.so |
3. Redis-stack.yaml
이전 포스팅에서 잠깐 설명했듯이 chatforyou.io 프로젝트의 redis 구조는 master 1 : slave 2 로 구성되어있다. 그리고 이에 맞춰 yaml 을 구성해야만 한다. 기존에는 redis 이미지만 사용했었지만 redis-stack 으로 바뀌는만큼 yaml 도 변경되어야할 필요가 있다.
아래의 두가지 yaml 을 사용시에는 꼭 확인해야하는 주의점이 있다.
1. redis 비밀번호 설정 : 실제 환경에서 사용시에는 --requirepass 옵션을 통해서 redis 비밀번호 설정이 꼭! 필요하다. 또한 여기서 비밀번호를 설정해두면 java 애플리케이션에서도 당연히 비밀번호를 입력해야한다.
2. 외부접속 허용 : 실제 환경에서 사용시에는 --protected-mode 옵션을 비활성화 yes 로 두어야한다. 현재는 개발단계이기에 no 로 두었지만 역시나 실제 운영환경에서는 localhost 에서만 접근할 수 있도록하는 해당 옵션을 yes 로 두어야한다.
1) redis-stack-master.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-master
namespace: redis
spec:
replicas: 1
serviceName: redis-master-headless
selector:
matchLabels:
app: redis
role: master
template:
metadata:
labels:
app: redis
role: master
spec:
containers:
- name: redis
image: redis/redis-stack:latest
command:
- redis-server
- '--protected-mode'
- 'no'
- '--bind'
- '0.0.0.0'
- '--loadmodule'
- '/opt/redis-stack/lib/redisearch.so'
- '--loadmodule'
- '/opt/redis-stack/lib/rejson.so'
- '--repl-diskless-sync'
- 'yes'
ports:
- containerPort: 6379
volumeMounts:
- name: redis-storage
mountPath: /data
securityContext:
runAsUser: 0
allowPrivilegeEscalation: false
volumes:
- name: redis-storage
persistentVolumeClaim:
claimName: redis-master-pvc
2) redis-stack-slave.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-slave
namespace: redis
spec:
replicas: 2
serviceName: redis-slave-headless
selector:
matchLabels:
app: redis
role: slave
template:
metadata:
labels:
app: redis
role: slave
spec:
containers:
- name: redis
image: redis/redis-stack:latest
command:
- redis-server
- '--replicaof'
- 'redis-master-0.redis-master-headless.redis.svc.cluster.local'
- '6379'
ports:
- containerPort: 6379
volumeMounts:
- name: redis-storage
mountPath: /data
volumes:
- name: redis-storage
persistentVolumeClaim:
claimName: redis-slave-pvc
3) 실행 결과
아래는 각각 master 와 slave 의 pod 로그 내용의 일부이다. 먼저 master 로그의 일부를 확인하면 search 모듈과 ReJSON 모듈이 제대로 로딩되는 것을 확인할 수 있다. 다음으로 slave pod 의 로그에서도 Search 모듈과 ReJSON 모듈이 모두 정상적으로 로딩되었고, 추가적으로 'MASTER <-> REPLICA sync: Finished with success' 라는 로그를 통해 master pod 와도 제대로 싱크가 된 것을 확인할 수 있다.
a. master pod log
1:C 07 Dec 2024 06:46:35.888 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 07 Dec 2024 06:46:35.888 * Redis version=7.4.1, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 07 Dec 2024 06:46:35.888 * Configuration loaded
1:M 07 Dec 2024 06:46:35.888 * monotonic clock: POSIX clock_gettime
1:M 07 Dec 2024 06:46:35.894 * Running mode=standalone, port=6379.
1:M 07 Dec 2024 06:46:35.895 * <search> Redis version found by RedisSearch : 7.4.1 - oss
1:M 07 Dec 2024 06:46:35.895 * <search> RediSearch version 2.10.5 (Git=2.10-e2f28a9)
1:M 07 Dec 2024 06:46:35.895 * <search> Low level api version 1 initialized successfully
1:M 07 Dec 2024 06:46:35.895 * <search> gc: ON, prefix min length: 2, min word length to stem: 4, prefix max expansions: 200, query timeout (ms): 500, timeout policy: return, cursor read size: 1000, cursor max idle (ms): 300000, max doctable size: 1000000, max number of search results: 1000000,
1:M 07 Dec 2024 06:46:35.895 * <search> Initialized thread pools!
1:M 07 Dec 2024 06:46:35.895 * <search> Enabled role change notification
1:M 07 Dec 2024 06:46:35.895 * Module 'search' loaded from /opt/redis-stack/lib/redisearch.so
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Created new data type 'ReJSON-RL'
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> version: 20803 git sha: unknown branch: unknown
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Exported RedisJSON_V1 API
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Exported RedisJSON_V2 API
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Exported RedisJSON_V3 API
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Exported RedisJSON_V4 API
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Exported RedisJSON_V5 API
1:M 07 Dec 2024 06:46:35.895 * <ReJSON> Enabled diskless replication
1:M 07 Dec 2024 06:46:35.895 * Module 'ReJSON' loaded from /opt/redis-stack/lib/rejson.so
1:M 07 Dec 2024 06:46:35.895 * <search> Acquired RedisJSON_V5 API
1:M 07 Dec 2024 06:46:35.896 * Server initialized
1:M 07 Dec 2024 06:46:35.906 * Creating AOF base file appendonly.aof.1.base.rdb on server start
1:M 07 Dec 2024 06:46:35.917 * Creating AOF incr file appendonly.aof.1.incr.aof on server start
1:M 07 Dec 2024 06:46:35.917 * Ready to accept connections tcp
1:M 07 Dec 2024 06:47:03.798 * Replica 10.244.1.216:6379 asks for synchronization
1:M 07 Dec 2024 06:47:03.798 * Full resync requested by replica 10.244.1.216:6379
1:M 07 Dec 2024 06:47:03.798 * Replication backlog created, my new replication IDs are '7f22202afe5acc2aea52130538b5a981bacf11fa' and '0000000000000000000000000000000000000000'
1:M 07 Dec 2024 06:47:03.798 * Delay next BGSAVE for diskless SYNC
1:M 07 Dec 2024 06:47:04.075 * Replica 10.244.3.250:6379 asks for synchronization
1:M 07 Dec 2024 06:47:04.075 * Full resync requested by replica 10.244.3.250:6379
1:M 07 Dec 2024 06:47:04.075 * Delay next BGSAVE for diskless SYNC
1:M 07 Dec 2024 06:47:08.093 * Starting BGSAVE for SYNC with target: replicas sockets
1:M 07 Dec 2024 06:47:08.093 * Background RDB transfer started by pid 12
12:C 07 Dec 2024 06:47:08.095 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
1:M 07 Dec 2024 06:47:08.095 * Diskless rdb transfer, done reading from pipe, 2 replicas still up.
1:M 07 Dec 2024 06:47:08.114 * Background RDB transfer terminated with success
1:M 07 Dec 2024 06:47:08.114 * Streamed RDB transfer with replica 10.244.1.216:6379 succeeded (socket). Waiting for REPLCONF ACK from replica to enable streaming
1:M 07 Dec 2024 06:47:08.114 * Synchronization with replica 10.244.1.216:6379 succeeded
1:M 07 Dec 2024 06:47:08.114 * Streamed RDB transfer with replica 10.244.3.250:6379 succeeded (socket). Waiting for REPLCONF ACK from replica to enable streaming
1:M 07 Dec 2024 06:47:08.114 * Synchronization with replica 10.244.3.250:6379 succeeded
b. slave pod log
1:C 07 Dec 2024 06:46:41.889 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 07 Dec 2024 06:46:41.889 * Redis version=7.4.1, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 07 Dec 2024 06:46:41.889 * Configuration loaded
1:S 07 Dec 2024 06:46:41.890 * monotonic clock: POSIX clock_gettime
1:S 07 Dec 2024 06:46:41.890 * Running mode=standalone, port=6379.
1:S 07 Dec 2024 06:46:41.892 * <search> Redis version found by RedisSearch : 7.4.1 - oss
1:S 07 Dec 2024 06:46:41.892 * <search> RediSearch version 2.10.5 (Git=2.10-e2f28a9)
1:S 07 Dec 2024 06:46:41.892 * <search> Low level api version 1 initialized successfully
1:S 07 Dec 2024 06:46:41.892 * <search> gc: ON, prefix min length: 2, min word length to stem: 4, prefix max expansions: 200, query timeout (ms): 500, timeout policy: return, cursor read size: 1000, cursor max idle (ms): 300000, max doctable size: 1000000, max number of search results: 1000000,
1:S 07 Dec 2024 06:46:41.892 * <search> Initialized thread pools!
1:S 07 Dec 2024 06:46:41.892 * <search> Enabled role change notification
1:S 07 Dec 2024 06:46:41.892 * Module 'search' loaded from /opt/redis-stack/lib/redisearch.so
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Created new data type 'ReJSON-RL'
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> version: 20803 git sha: unknown branch: unknown
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Exported RedisJSON_V1 API
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Exported RedisJSON_V2 API
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Exported RedisJSON_V3 API
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Exported RedisJSON_V4 API
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Exported RedisJSON_V5 API
1:S 07 Dec 2024 06:46:41.893 * <ReJSON> Enabled diskless replication
1:S 07 Dec 2024 06:46:41.893 * Module 'ReJSON' loaded from /opt/redis-stack/lib/rejson.so
1:S 07 Dec 2024 06:46:41.893 * <search> Acquired RedisJSON_V5 API
1:S 07 Dec 2024 06:46:41.893 * Server initialized
1:S 07 Dec 2024 06:46:41.893 * Ready to accept connections tcp
1:S 07 Dec 2024 06:47:04.073 * MASTER <-> REPLICA sync started
1:S 07 Dec 2024 06:47:04.073 * Non blocking connect for SYNC fired the event.
1:S 07 Dec 2024 06:47:04.074 * Master replied to PING, replication can continue...
1:S 07 Dec 2024 06:47:04.075 * Partial resynchronization not possible (no cached master)
1:S 07 Dec 2024 06:47:08.093 * Full resync from master: 7f22202afe5acc2aea52130538b5a981bacf11fa:14
1:S 07 Dec 2024 06:47:08.094 * MASTER <-> REPLICA sync: receiving streamed RDB from master with EOF to disk
1:S 07 Dec 2024 06:47:08.094 * MASTER <-> REPLICA sync: Flushing old data
1:S 07 Dec 2024 06:47:08.095 * MASTER <-> REPLICA sync: Loading DB in memory
1:S 07 Dec 2024 06:47:08.113 * <search> Loading event starts
1:S 07 Dec 2024 06:47:08.113 * Loading RDB produced by version 7.4.1
1:S 07 Dec 2024 06:47:08.113 * RDB age 0 seconds
1:S 07 Dec 2024 06:47:08.113 * RDB memory usage when created 1.38 Mb
1:S 07 Dec 2024 06:47:08.113 * Done loading RDB, keys loaded: 0, keys expired: 0.
1:S 07 Dec 2024 06:47:08.113 * <search> Loading event ends
1:S 07 Dec 2024 06:47:08.113 * MASTER <-> REPLICA sync: Finished with success
4. RedisSearch 검색 쿼리 : redis-cli 사용하기
사실 이번 포스팅의 핵심 내용중 하나이다. springboot 와 바로 연결해서 쿼리를 사용하는 것보다는 일단 redis-cli 를 통해서 쿼리를 실행해보려고한다. 현재 레디스에는 아래의 내용에 해당하는 총 6개의 test room 정보가 저장되어 있다.
{
"result": "success",
"count": 6,
"roomList": [
{
"userIdx": 31,
"sessionId": "b28aa588-fad9-4518-a430-a2e7e196a41e",
"roomName": "tr6",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 31,
"sessionId": "07c8d174-72c0-481c-95d0-8795bfa5bdf9",
"roomName": "test room 5",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 31,
"sessionId": "8ac1a862-a105-47b0-84b8-fbd53801e46e",
"roomName": "test room 4",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 31,
"sessionId": "993602bf-6384-48df-899c-d9689b7e9380",
"roomName": "test room 3",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 31,
"sessionId": "75b69396-5df8-4816-9f9a-c5cc1d094dae",
"roomName": "test room 2",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 31,
"sessionId": "800034a4-2385-4020-a8f0-478f3abac08f",
"roomName": "test room 1",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
}
]
}
1) FT._LIST : Redis Index 확인하기
RedisConfig 에서 설정해두었던 chatRoomIndex 가 를 확인 할 수 있다.
127.0.0.1:6379> FT._LIST
1) chatRoomIndex
2) FT.INFO : 상세 정보 확인
해당 명령어로는 index 의 상세 정보를 확인할 수 있다.
127.0.0.1:6379> FT.INFO chatRoomIndex
1) index_name
2) chatRoomIndex
3) index_options
4) (empty array)
5) index_definition
6) 1) key_type
2) HASH
3) prefixes
4) 1)
5) default_score
6) "1"
7) attributes
8) 1) 1) identifier
2) sessionId
3) attribute
4) sessionId
5) type
6) TEXT
7) WEIGHT
8) "1"
2) 1) identifier
2) creator
3) attribute
4) creator
5) type
6) TEXT
7) WEIGHT
8) "1"
9) NOSTEM
3) 1) identifier
2) roomName
3) attribute
4) roomName
5) type
6) TEXT
7) WEIGHT
8) "1"
9) NOSTEM
4) 1) identifier
2) currentTime
3) attribute
4) currentTime
5) type
6) NUMERIC
3) FT.SEARCH chatRoomIndex "*" : chatroomIndex 의 모든 문서 검색
FT.SEARCH ${인덱스명} 을 사용하면 해당 인덱스와 관련된 모든 문서를 검색가능하다. 모든 문서이기에 "*" 와일드 카드를 사용한다.
1) (integer) 6
2) "sessionId:800034a4-2385-4020-a8f0-478f3abac08f"
3) 1) "roomName"
2) "\"test room 1\""
3) "currentTime"
4) "1733554834500"
5) "screen_31"
6) "{\"@class\":\"com.chatforyou.io.models.out.ConnectionOutVo\",\"connectionId\":\"con_screen_31\",\"status\":\"pending\",\"createdAt\":1733554834075,\"activeAt\":null,\"location\":null,\"ip\":null,\"platform\":null,\"clientData\":null,\"token\":\"wss://hjproject.kro.kr:4443?sessionId=800034a4-2385-4020-a8f0-478f3abac08f&token=tok_QbzqpPa59RK89nUJ\"}"
7) "openvidu"
8) "{\"@class\":\"com.chatforyou.io.models.OpenViduDto\",\"recordingActive\":false,\"broadcastingActive\":false,\"creator\":null,\"recording_enabled\":true,\"is_recording_active\":false,\"broadcasting_enabled\":false,\"is_broadcasting_active\":false,\"session\":{\"@class\":\"com.chatforyou.io.models.out.SessionOutVo\",\"sessionId\":\"800034a4-2385-4020-a8f0-478f3abac08f\",\"createdAt\":1733554833641,\"connections\":{\"@class\":\"java.util.concurrent.ConcurrentHashMap\",\"con_screen_31\":{\"@class\":\"com.chatforyou.io.models.out.ConnectionOutVo\",\"connectionId\":\"con_screen_31\",\"status\":\"pending\",\"createdAt\":1733554834075,\"activeAt\":null,\"location\":null,\"ip\":null,\"platform\":null,\"clientData\":null,\"token\":\"wss://hjproject.kro.kr:4443?sessionId=800034a4-2385-4020-a8f0-478f3abac08f&token=tok_QbzqpPa59RK89nUJ\"},\"con_camera_31\":{\"@class\":\"com.chatforyou.io.models.out.ConnectionOutVo\",\"connectionId\":\"con_camera_31\",\"status\":\"pending\",\"createdAt\":1733554833979,\"activeAt\":null,\"location\":null,\"ip\":null,\"platform\":null,\"clientData\":null,\"token\":\"wss://hjproject.kro.kr:4443?sessionId=800034a4-2385-4020-a8f0-478f3abac08f&token=tok_CjVTUjSDiCKvu9tc\"}},\"recording\":false,\"broadcasting\":false}}"
9) "chatroom"
10) "{\"@class\":\"com.chatforyou.io.models.in.ChatRoomInVo\",\"sessionId\":\"800034a4-2385-4020-a8f0-478f3abac08f\",\"userIdx\":31,\"creator\":null,\"roomName\":\"test room 1\",\"pwd\":null,\"usePwd\":false,\"usePrivate\":false,\"useRtc\":false,\"desc\":\"\xec\x84\xa4\xeb\xaa\x85\xec\x84\xa4\xeb\xaa\x85\xec\x84\xa4\xeb\xaa\x85\",\"maxUserCount\":4}"
11) "camera_31"
12) "{\"@class\":\"com.chatforyou.io.models.out.ConnectionOutVo\",\"connectionId\":\"con_camera_31\",\"status\":\"pending\",\"createdAt\":1733554833979,\"activeAt\":null,\"location\":null,\"ip\":null,\"platform\":null,\"clientData\":null,\"token\":\"wss://hjproject.kro.kr:4443?sessionId=800034a4-2385-4020-a8f0-478f3abac08f&token=tok_CjVTUjSDiCKvu9tc\"}"
13) "sessionId"
14) "\"800034a4-2385-4020-a8f0-478f3abac08f\""
15) "creator"
16) ""
4) RETURN N "필드명1" "필드명2" --- : 특정 필드만 반환하기
return 뒤에오는 N 은 몇개의 필드를 반환할것인지 설정하고, 그 뒤에는 실제 필드명이 오게 된다. 만약 sessionId, roomName 이면 N 에는 2가 오는 것이고 createDate 까지 포함한다면 3 이 올게 될 것이다.
127.0.0.1:6379> FT.SEARCH chatRoomIndex "*" RETURN 2 sessionId roomName
1) (integer) 6
2) "sessionId:800034a4-2385-4020-a8f0-478f3abac08f"
3) 1) "sessionId"
2) "\"800034a4-2385-4020-a8f0-478f3abac08f\""
3) "roomName"
4) "\"test room 1\""
4) "sessionId:75b69396-5df8-4816-9f9a-c5cc1d094dae"
5) 1) "sessionId"
2) "\"75b69396-5df8-4816-9f9a-c5cc1d094dae\""
3) "roomName"
4) "\"test room 2\""
6) "sessionId:993602bf-6384-48df-899c-d9689b7e9380"
7) 1) "sessionId"
2) "\"993602bf-6384-48df-899c-d9689b7e9380\""
3) "roomName"
4) "\"test room 3\""
8) "sessionId:8ac1a862-a105-47b0-84b8-fbd53801e46e"
9) 1) "sessionId"
2) "\"8ac1a862-a105-47b0-84b8-fbd53801e46e\""
3) "roomName"
4) "\"test room 4\""
10) "sessionId:07c8d174-72c0-481c-95d0-8795bfa5bdf9"
11) 1) "sessionId"
2) "\"07c8d174-72c0-481c-95d0-8795bfa5bdf9\""
3) "roomName"
4) "\"test room 5\""
12) "sessionId:b28aa588-fad9-4518-a430-a2e7e196a41e"
13) 1) "sessionId"
2) "\"b28aa588-fad9-4518-a430-a2e7e196a41e\""
3) "roomName"
4) "\"tr6\""
5) limit N M : 검색 갯수 제한하기
마치 sql 처럼 limit 명령어를 사용하면 특정 갯수만 가져올 수 있다. 해당 명령어는 페이징 처리할 때 유용하게 사용할 수 있다.
127.0.0.1:6379> FT.SEARCH chatRoomIndex "*" RETURN 3 sessionId roomName createDate LIMIT 0 2
1) (integer) 6
2) "sessionId:800034a4-2385-4020-a8f0-478f3abac08f"
3) 1) "sessionId"
2) "\"800034a4-2385-4020-a8f0-478f3abac08f\""
3) "roomName"
4) "\"test room 1\""
4) "sessionId:75b69396-5df8-4816-9f9a-c5cc1d094dae"
5) 1) "sessionId"
2) "\"75b69396-5df8-4816-9f9a-c5cc1d094dae\""
3) "roomName"
4) "\"test room 2\""
6) FT.SEARCH ${인덱스명} "@roomName:${특정문자}*" : 특정 문자가 포함된 내용만 검색하기
검색의 꽃은 역시나 특정 문자가 포함된 내용을 검색하는 것이다. 채팅방을 검색하는 경우 높은 확률로 채팅방의 이름에 특정단어가 포함된 방을 검색하게 될 것이다. 이때 사용하는 명령어이다. sql 로 생각하자면 일종의 where 조건문이 된다.
127.0.0.1:6379> FT.SEARCH chatRoomIndex "@roomName:tr*" return 2 roomName sessionId
1) (integer) 1
2) "sessionId:b28aa588-fad9-4518-a430-a2e7e196a41e"
3) 1) "roomName"
2) "\"tr6\""
3) "sessionId"
4) "\"b28aa588-fad9-4518-a430-a2e7e196a41e\""
6) FT.SEARCH chatRoomIndex "((@creator:*teri*) | (@roomName:*teri*))" : and, or 조건 사용하기
당연하게도 방 이름만 검색하는게 아니라 방을 만든사람을 검색하고 싶을때도 있고 방이름에 해당 단어가 포함되어 있거나, 만든 사람이름에 포함되어 있거나를 검색하는 경우가 있을 것이다. 아래는 각각 and 조건과 or 조건일때 사용하는 방법이다.
And 조건 : FT.SEARCH chatRoomIndex "((@creator:*teri*) (@roomName:*teri*))"
OR 조건 : FT.SEARCH chatRoomIndex "((@creator:*teri*) | (@roomName:*teri*))"
5. Spring 에서 redisSearch 사용하기
이번에는 아래의 데이터를 기준으로 spring 에서 redisSearch 를 사용해보겠다.
{
"result": "success",
"count": 3,
"roomList": [
{
"userIdx": 32,
"creator": "nicknameCC",
"sessionId": "ebf129c4-36ea-487d-a1e9-3dcff4cc0b65",
"roomName": "eeff5",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 22,
"creator": "terianP4",
"sessionId": "3087da79-e744-4e34-8915-5a3b8d17e44e",
"roomName": "ccdd3",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
},
{
"userIdx": 25,
"creator": "nicknameCC",
"sessionId": "18abd1a0-fc32-4bdf-87d7-b8957d8deab9",
"roomName": "aabb2",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
}
]
}
1) redisSearch 사용하기 : 모든 내용 검색
java 에서 redisSearch 를 사용하기 위해서는 redisSearchClient 를 사용해야한다. 이때 객체 생성시에는 어떤 인덱스에서 검색할지를 지정해야한다.현재는 chatroom 을 검색하기위해 chatRoomIndex 를 지정해서 객체를 생성했다. 여기서 핵심은 해당 객체의 search 메서드이다.
search 는 총 2개의 매개변수를 받는데 첫번째는 검색 조건이고 두번째는 searchOption 이다. 조건쪽은 redis-cli 를 통해서 잠깐 보았던 그런 조건이 들어가고 option 쪽에는 페이징처리, return 받는 필드, 정렬 조건등 말 그대로 검색의 옵션을 정의한다.
또한 redisSearch 의 결과로 받는 데이터는 Document 클래스 객체로 받아야한다. nosql 쪽에서 저장하는 데이터를 '문서'라고 부르곤하던데 그것과 관련있는건가 싶기도하다.
@Test
@DisplayName("레디스 전체 데이터 검색")
void searchAllRedisData() throws BadRequestException {
// RediSearch 클라이언트를 사용하여 chatRoomIndex 검색 실행
RediSearch chatRoomSearch = rediSearchClient.getRediSearch("chatRoomIndex");
// 모든 문서를 검색
List<Document> documents = chatRoomSearch.search("*", new SearchOptions()).getDocuments();
// Jackson ObjectMapper 생성
ObjectMapper objectMapper = new ObjectMapper();
// 모든 검색 결과 순회
for (Document doc : documents) {
logger.info("sessionId : {}",doc.getId());
logger.info("roomName : {}",doc.getFields().get("roomName"));
}
}
2) redisSearch 사용하기 : 단일 조건 검색
이번에는 creator 로만 조건을 걸어 검색해보았다.
@Test
@DisplayName("레디스 단일 조건 검색")
void searchRedis() throws BadRequestException {
// 검색 실행
RediSearch chatRoomSearch = rediSearchClient.getRediSearch("chatRoomIndex");
// Redis 검색 결과에서 openvidu 필드의 JSON 문자열 가져오기
List<Document> documents = chatRoomSearch.search(
"@creator:*teri*",
new SearchOptions()
).getDocuments();
// 모든 검색 결과 순회
for (Document doc : documents) {
logger.info("sessionId : {}",doc.getId());
logger.info("roomName : {}",doc.getFields().get("roomName"));
logger.info("creator : {}",doc.getFields().get("creator"));
}
}
3) redisSearch 사용하기 : 다중 조건 + 페이징 처리 검색
이번에는 다중 조건과 함께 페이징 처리를 위한 검색 옵션을 함께 넣어보았다. 검색 조건은 queryParam 으로 만들고, SearchOption 에 페이지넘버와 사이즈를 계산해서 넣는 방식을 사용한다.
@Test
@DisplayName("레디스 다중 조건 검색")
void searchKeyword() throws BadRequestException {
// 검색 실행
RediSearch chatRoomSearch = rediSearchClient.getRediSearch("chatRoomIndex");
// Redis 검색 결과에서 openvidu 필드의 JSON 문자열 가져오기
int pageNumber = 0; // 원하는 페이지 번호
int pageSize = 5; // 한 페이지에 표시할 항목 수
String keyword = "aabb";
String queryParam = "((@creator:*" + keyword + "*) | (@roomName:*" + keyword + "*))";
List<Document> documents = chatRoomSearch.search(
queryParam,
new SearchOptions().page(pageNumber * pageSize, pageSize)
).getDocuments();
// 모든 검색 결과 순회
for (Document doc : documents) {
logger.info("sessionId : {}",doc.getId());
logger.info("roomName : {}",doc.getFields().get("roomName"));
}
}
4) 고급 redisSearch 사용하기 : 인덱스 + 다중 조건 + 특정 필드값만 return + 페이징
현재 chatforyou.io 프로젝트에서 실제로 사용하는 코드의 일부이다. 여기에서는 searchKeyword 라는 메서드를 통해서 redis 의 데이터를 검색한다. 이때 searchType 을 통해서 현재 검색하고자 하는 것이 chatroom 의 데이터인지 아니면 loginuser 에 관한 데이터인지 구분한다. 그리고 그에 따라서 queryParam 과 option 을 다르게 구성한다.
public List<Document> searchByKeyword(SearchType searchType, String keyword, int pageNum, int pageSize) {
// searchType 에 맞춰 indexName 을 가져옴
RediSearch rediSearch = rediSearchClient.getRediSearch(searchType.getIndexName());
// Redis 검색 결과에서 openvidu 필드의 JSON 문자열 가져오기
String queryParam = "*";
SearchOptions searchOptions = null;
// or 조건이 제대로 동작하려면 조건과 조건을 () 로 구분해서 묶어야함
switch (searchType) {
case CHATROOM:
if (!StringUtil.isNullOrEmpty(keyword)) {
// 검색어가 있을 때: creator 또는 roomName 필드 검색, 그리고 user: 값 제외
queryParam = "((@creator:*" + keyword + "*) | (@roomName:*" + keyword + "*))";
}
searchOptions = new SearchOptions()
.page(pageNum * pageSize, pageSize) // 페이지 설정
.returnFields("sessionId") // sessionId 필드만 반환
.sort(new SortBy("currentTime", SortOrder.DESC)); // currentTime 기준 내림차순 정렬
break;
case LOGIN_USER:
if (!StringUtil.isNullOrEmpty(keyword)) {
// 검색어가 있을 때: userId 또는 nickName 필드 검색
queryParam = "((@userId:*" + keyword + "*) | (@nickName:*" + keyword + "*))";
}
searchOptions = new SearchOptions()
.page(pageNum * pageSize, pageSize) // 페이지 설정
.returnFields("user")
.sort(new SortBy("userId", SortOrder.DESC)); // userId 기준 내림차순 정렬
break;
}
List<Document> documents = rediSearch.search(
queryParam,
searchOptions
).getDocuments();
return documents;
}
요청과 결과
아래는 chatroom/list 로 요청을 보낼때 사용하는 요청과 그에 따른 결과이다.
GET /chatroom/list?pageNum=1&pageSize=20&keyword=P4 HTTP/1.1
Host: localhost:8443
Content-Type: application/json
Content-Length: 184
{
"result": "success",
"count": 1,
"roomList": [
{
"userIdx": 22,
"creator": "terianP4",
"sessionId": "52fa9146-b205-4317-adc7-ee8e41fe1334",
"roomName": "ccdd3",
"usePwd": false,
"usePrivate": false,
"useRtc": false,
"currentUserCount": 0,
"maxUserCount": 4,
"userList": []
}
]
}
6. 마무리하면서
사실 redisSearch 를 찾기까지 그리고 실제로 적용하기까지 상당히 많은 시간이 걸렸습니다. 이는 기존의 redis 환경을 바꾸는 것을 포함해서 코드상에서 어떻게 적용하면되는지 레퍼런스도 찾기 어려웠고 실제로 내가 원하는 바에 맞춰서 적용하기도 어려웠어요ㅠㅠㅠ 다행히도 스택오버플로, 각종 블로그 그리고 chatGPT 의 힘을 빌려 겨우겨우 완성할 수 있었던 것 같습니다. 이제 다음에는 token 을 발급하고 이를 적용하기 위한 내용을 작성해볼까합니다! 화이팅!!
Reference
https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/docker/
https://yonguri.tistory.com/147
https://github.com/RediSearch/RediSearch?tab=readme-ov-file
https://13months.tistory.com/677
https://yoonjk.github.io/cache/installl-redisearch-on-docker/