-
Notifications
You must be signed in to change notification settings - Fork 4
기술공유 7주차
엘라스틱서치는 Apache Lucene(이하 루씬)을 기반으로 만들었다.
루씬은 역색인(각각의 단어가 어느 문서에 속해있는지 목록을 유지하는 자료구조 생성)을 제공한다.
참고자료 : https://tecadmin.net/setup-elasticsearch-on-ubuntu/
- JAVA 11 이상이 설치된 os에서 동작할 수 있음
- 환경변수 파일로 network 대역 설정해 줘야 외부에서 접속 가능함.
- 권한 설정을 위해 환경변수 파일에 cluster와 node에 대한 정보를 명시해야함.
-
관계형 데이터베이스는 단순 텍스트매칭에 대한 검색만을 제공
- 물론 요즘 MySQL 최신 버전에서 n-gram 기반의 Full-text 검색을 지원하지만, 한글 검색의 경우에 아직 많이 빈약한 감이 있습니다.
- 텍스트를 여러 단어로 변형하거나 텍스트의 특질을 이용한 동의어나 유의어를 활용한 검색이 가능
-
엘라스틱서치에서는 형태소 분석을 통한 자연어 처리가 가능
- 엘라스틱서치는 다양한 형태소 분석 플러그인을 제공합니다.
-
역색인 지원으로 매우 빠른 검색이 가능
엘라스틱서치와 관계형 데이터베이스
elasticSearch 구축
(1) 사이트에서 정보를 요청한다.
(2) 우선 S3(object storage)에 저장된 요청 정보가 있는지 확인한다. (한번 읽어온 정보는 최대 100MB 공간을 마련해서 S3에서 다시 읽을 필요없도록 저장하고 있는다. 최대 용량 초과 시 참조하지 않는 결과부터 삭제). 원하는 결과가 있을 경우 바로 (5)번으로.
(3) S3에 저장된 결과가 없다면 ES에 직접 쿼리한다.
(4) (3)에서 얻어온 결과를 S3에 저장하고, 메모리에도 저장해둔다.
(5) 검색된 결과를 돌려준다.
- 건바이건으로 엘라스틱서치 데이터를 갱신하는 방법은 엘라스틱서치에서 너무 많은 HTTP Request를 처리하게 만든다.
- 이렇게 되면 엘라스틱서치의 리소스를 효율적으로 쓰지 못하게 되는 이슈가 발생한다.\
- 루씬 INDEX는 여러 세그먼트로 구성되는데, 이 세그먼트에 있는 문서들은 imuutable하다. 즉, 세그먼트 안에 있는 문서들은 변경할 수 없다.
- 그래서 문서를 업데이트한다는건, 아래의 3가지 처리가 진행된단걸 의미한다.
- 세그먼트에 있는 기존 문서가 지워졌다고 마킹 처리를 한다.
- 새 문서를 만들어서, 버퍼링 처리해둔다. ( 이 새로운 문서는 새 세그먼트가 만들어질 때 사용된다 )
- 문서를 re-analyze한다.
이런 작업이 리소스를 잡아먹는다.
- Bulk API는 한 번의 API 호출로 여러 문서를 업데이트할 수 있게 해준다.
- 그래서 배치로 원천 데이터들을 ElasticSearch으로 복제뜰때, Bulk API는 매우! 중요한 역할을 한다.
- 왜냐하면 HTTP 요청수를 줄여주기 때문이다.
- 100개의 문서를 수정할 때, 수정요청 Request를 100번 호출해야한다.
- 그런데 Bulk API를 사용하면, 1번만 호출하면 된다.
config/elasticsearch.yml 수정
# ---------------------------------- Network -----------------------------------
#
# Set the bind address to a specific IP (IPv4 or IPv6):
#
network.host: 0.0.0.0
#
# Set a custom port for HTTP:
#
# http.port: 9200
logstash 파이프라인 생성
/* logstash.conf */
input {
jdbc {
jdbc_driver_library => "lib/mysql-connector-java-5.1.33.jar"
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_connection_string => "jdbc:mysql://localhost:3306/[db-name]"
jdbc_user => "[db-user]"
jdbc_password => "[password]"
statement => "SELECT * FROM [tbl-name]"
schedule => "* * * * *"
}
}
output {
elasticsearch {
hosts => ["localhost:9200"]
} stdout {
codec => rubydebug
}
}
function putSettings() {
console.log('Put settings ...');
return client.indices.putSettings({
index: index,
body: {
settings: {
analysis: {
analyzer: {
autocomplete: {
tokenizer: 'autocomplete',
filter: ['lowercase']
},
autocomplete_search: {
tokenizer: 'lowercase'
},
korean: {
filter: ['npos_filter', 'nori_readingform', 'lowercase'],
tokenizer: "nori_user_dict"
}
},
tokenizer: {
autocomplete: {
type: 'edge_ngram',
min_gram: 1,
max_gram: 30,
token_chars: [
'letter',
'digit',
'whitespace'
]
},
nori_user_dict: {
mode: "mixed",
type: "nori_tokenizer",
user_dictionary: "userdic_ko.txt"
}
},
filter: {
npos_filter: {
type: "nori_part_of_speech",
stoptags: [
"E", "IC", "J", "MAG", "MM", "SP", "SSC",
"SSO", "SC", "SE", "XPN", "XSA",
"XSN", "XSV", "UNA", "NA", "VSV"
]
}
}
}
}
}
}).then(handleResolve);
}
function putMapping() {
console.log('Put mapping ...');
return client.indices.putMapping({
index: index,
body: {
_source: {
enabled: true
},
properties: {
name: { type: 'text', analyzer: 'autocomplete', search_analyzer: "korean" },
likes: { type: 'long' },
reg_date: { type: 'date' },
streaming_url: { type: 'text' },
thumbnai_video_url: { type: 'text' },
thumbnail_img_url: { type: 'text' },
video_id: { type: 'text' },
category: { type: 'text', analyzer: 'autocomplete', search_analyzer: "korean" },
index : {type: 'long'}
}
}
}).then(handleResolve);
}
$ curl -XGET http://211.249.49.243:9200/netflix-db-server/?pretty
{
"netflix-db-server" : {
"aliases" : { },
"mappings" : {
"properties" : {
"category" : {
"type" : "text",
"analyzer" : "autocomplete",
"search_analyzer" : "korean"
},
"index" : {
"type" : "long"
},
"likes" : {
"type" : "long"
},
"name" : {
"type" : "text",
"analyzer" : "autocomplete",
"search_analyzer" : "korean"
},
"reg_date" : {
"type" : "date"
},
"streaming_url" : {
"type" : "text"
},
"thumbnai_video_url" : {
"type" : "text"
},
"thumbnail_img_url" : {
"type" : "text"
},
"thumbnail_video_url" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"video_id" : {
"type" : "text"
}
}
},
"settings" : {
"index" : {
"number_of_shards" : "1",
"provided_name" : "netflix-db-server",
"creation_date" : "1576058178365",
"analysis" : {
"filter" : {
"npos_filter" : {
"type" : "nori_part_of_speech",
"stoptags" : [
"E",
"IC",
"J",
"MAG",
"MM",
"SP",
"SSC",
"SSO",
"SC",
"SE",
"XPN",
"XSA",
"XSN",
"XSV",
"UNA",
"NA",
"VSV"
]
}
},
"analyzer" : {
"korean" : {
"filter" : [
"npos_filter",
"nori_readingform",
"lowercase"
],
"tokenizer" : "nori_user_dict"
},
"autocomplete" : {
"filter" : [
"lowercase"
],
"tokenizer" : "autocomplete"
},
"autocomplete_search" : {
"tokenizer" : "lowercase"
}
},
"tokenizer" : {
"nori_user_dict" : {
"mode" : "mixed",
"type" : "nori_tokenizer",
"user_dictionary" : "userdic_ko.txt"
},
"autocomplete" : {
"token_chars" : [
"letter",
"digit",
"whitespace"
],
"min_gram" : "1",
"type" : "edge_ngram",
"max_gram" : "30"
}
}
},
"number_of_replicas" : "0",
"uuid" : "vQpQYDaqSFS8cXDiai2axg",
"version" : {
"created" : "7040299"
}
}
}
}
}
데이터 조회 curl -XGET http://211.249.49.243:9200/netflix-db-server/_search?pretty
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 240,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "netflix-db-server",
"_type" : "_doc",
"_id" : "-2UvYTXiaxw",
"_score" : 1.0,
"_source" : {
"index" : 1,
"likes" : 2377,
"thumbnail_img_url" : "https://i.ytimg.com/vi/-2UvYTXiaxw/maxresdefault.jpg",
"streaming_url" : "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
"category" : "?좎셿?숇Ъ?숇Ъ",
"reg_date" : "2007-10-04T00:00:00.000Z",
"video_id" : "-2UvYTXiaxw",
"thumbnail_video_url" : "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
"name" : "Stuffing Dead Pets | Taboo: Pets"
}
}
}
]
}
select video_id, name, category, likes, reg_date,
thumbnail_img_url, thumbnail_video_url, like_cnt
from videos a,
(
select fk_video_id, count(fk_video_id) as "like_cnt"
from likes
where fk_user_id in(
select fk_user_id
from likes
where fk_video_id=1197
and fk_user_id not in (2)
)
group by fk_video_id
order by like_cnt desc
) b
where a.video_id = b.fk_video_id
order by like_cnt desc;
async function get_search(column, target, order, size){
return await client.search({
index: process.env.index,
type: '_doc',
sort : [`${column} : ${order}`],
body: {
size : size,
query: {
match : {
[column] : target
}
}
}
}).then(function(resp){
return resp.hits.hits;
},function(err){
return err;
})
};