주소로 고객 검색 서비스 구축하기(feat. Elastic Search, MacOS) - 2탄
안녕하세요. 클스 입니다.
* 목표
- Elastic Search & Kibana 설치 (1탄) - 보기
- Elastic Search Client for Python 설치 및 프로그램 (2탄) - 현재글
- 주소 데이터 검색 구조 설계 및 bulk 생성/업로드 (3탄)
- 주소 데이터 검색 구조 설계 및 bulk 생성/업로드 (3.5탄) - Polars vs Pandas
- 대량으로 검색 하기 (4탄)
- 데이터 백업 및 복구 <유지관리> (5탄)
지난 1탄에서는 설치하고 구동하고 화면 접속을 확인 했습니다. 이번 2탄에서는 실제로 데이터를 설계하고
넣는 방법을 알아보도록 하겠습니다.
1) Console UI 에서 데이터 입력 하기
왼쪽 메뉴 하단으로 내려가면 Management > Dev Tools 를 선택하면 아래화면이 나옵니다.
좌측 상단은 히스토리, 우측 상단은 상세내용으로 구성되어 있습니다.
좌측 하단에 REST API 와 데이터를 JSON으로 입력하고 실행하면 우측에 결과가 들어갑니다.
같은 내용이면 업데이트 하고, 다른 내용이면 신규로 추가 됩니다.
이제 주소 데이터 <local.go.kr 에서 제공하는 데이터> 를 올리기 위해서 csv를 JSON으로 만들어서
넣겠습니다.
* 데이터는 local.go.kr 에서 대전시 식당 데이터를 사용합니다.
다운 받아서 불필요한 컬럼은 삭제하였습니다.
주소는 지번, 도로명 주소 2개를 사용하며, 사업장명 동-호수도 사용합니다.
우선 Elastic Search의 데이터는 JSON으로 만들어져야 합니다. 그래서 일반적인 csv, tsv 같은 텍스트
데이터는 바로 올릴 수 없습니다.
elasticsearch_addr.py 파일 입니다.
'''
주소를 포함한 정보를 엑셀에서 읽어서 JSON으로 변환 함
pip install xlsx2csv
pip install xlrd
pip install pandas
pip install openpyxl, tabulate
'''
import pandas as pd
import json
from tabulate import tabulate
tabulate.WIDE_CHARS_MODE = True
excel_file = "./지역데이터_대전계룡_식당_SIMPLE.xlsx";
csv_file = "./지역데이터_대전계룡_식당_SIMPLE.csv";
json_file = "./지역데이터_대전계룡_식당_SIMPLE.json";
json_file2 = "./지역데이터_대전계룡_식당_SIMPLE2.json";
sheet_name = "대전"
df_columns = ['biz_category', 'biz_category_id', 'biz_category_areacd', 'mgmt_no', 'allow_date', 'biz_status',
'biz_status_detail', 'biz_addr', 'biz_addr_doro', 'biz_name', 'biz_domain']
df_addr = pd.read_excel(excel_file, sheet_name = sheet_name, header=0,
usecols=['개방서비스명', '개방서비스아이디', '개방자치단체코드', '관리번호', '인허가일자', '영업상태명',
'상세영업상태명', '소재지전체주소', '도로명전체주소', '사업장명', '위생업태명'], engine='openpyxl');
df_addr.columns = df_columns
print(tabulate(df_addr.head(5), headers='keys', tablefmt='psql'))
df_addr.to_json(json_file, force_ascii=False, orient = 'records', indent=2)
실행 결과 : 출력
실행 결과 : JSON 파일
[ {
"biz_category":"일반음식점",
"biz_category_id":"07_24_04_P",
"biz_category_areacd":3640000,
"mgmt_no":"3640000-101-2022-00099",
"allow_date":"2022-06-07",
"biz_status":"영업\/정상",
"biz_status_detail":"영업",
"biz_addr":"대전광역시 동구 가양동 292-1 ",
"biz_addr_doro":"대전광역시 동구 동대전로263번길 51 (가양동)",
"biz_name":"물회&회덮밥8000원",
"biz_domain":"한식"
},
{
"biz_category":"일반음식점",
"biz_category_id":"07_24_04_P",
"biz_category_areacd":3640000,
"mgmt_no":"3640000-101-2022-00100",
"allow_date":"2022-06-07",
"biz_status":"영업\/정상",
"biz_status_detail":"영업",
"biz_addr":"대전광역시 동구 판암동 839 삼정그린코아 포레스트(1단지) ",
"biz_addr_doro":"대전광역시 동구 동부로10번길 55, 401동 105호 (판암동, 삼정그린코아 포레스트(1단지))",
"biz_name":"비비큐(BBQ) 대전 판암점",
"biz_domain":"호프\/통닭"
},
.....
]
* Dev Tools > Console 에서 하나 입력해보기 /_doc/2, 3 이렇게 3개를 입력했습니다.
client python을 이용해서 bulk 업로드도 나중에 해보겠습니다.
POST /restaurants/_doc/1
{
"biz_category":"일반음식점",
"biz_category_id":"07_24_04_P",
"biz_category_areacd":3640000,
"mgmt_no":"3640000-101-2022-00099",
"allow_date":"2022-06-07",
"biz_status":"영업\/정상",
"biz_status_detail":"영업",
"biz_addr":"대전광역시 동구 가양동 292-1 ",
"biz_addr_doro":"대전광역시 동구 동대전로263번길 51 (가양동)",
"biz_name":"물회&회덮밥8000원",
"biz_domain":"한식"
}
* elasticsearch_addr_search.py 파일 입니다. 등록된 데이터를 조회해 봅니다.
'''
es 서버에 접속하여 주소를 입력하면 전체 고객 정보를 받음
$ pip install elasticsearch, tabulate, pandas
'''
from elasticsearch import Elasticsearch
from elasticsearch import helpers
from pandas.io.json import json_normalize
from tabulate import tabulate
tabulate.WIDE_CHARS_MODE = True
es = Elasticsearch('https://localhost:9200', basic_auth=('elastic', '~~your-password~~'), ca_certs=False, verify_certs=False)
es_index = 'restaurants'
doc={"query":{"match_all":{}}}
res=es.search(index=es_index, body=doc, size=10)
df = json_normalize(res['hits']['hits'])
print(tabulate(df, headers='keys', tablefmt='psql'))
* 결과 확인 : 옆으로 너무 길어서 분리 했습니다.
각 데이터별로 _score가 나오네요~
+----+-------------+-------+----------+------------------------+---------------------------+-------------------------------+------------------------+----------------------+----------------------+-----------------------------
| | _index | _id | _score | _source.biz_category | _source.biz_category_id | _source.biz_category_areacd | _source.mgmt_no | _source.allow_date | _source.biz_status | _source.biz_status_detail
|----+-------------+-------+----------+------------------------+---------------------------+-------------------------------+------------------------+----------------------+----------------------+-----------------------------
| 0 | restaurants | 1 | 1 | 일반음식점 | 07_24_04_P | 3640000 | 3640000-101-2022-00099 | 2022-06-07 | 영업/정상 | 영업
| 1 | restaurants | 2 | 1 | 일반음식점 | 07_24_04_P | 3640000 | 3640000-101-2022-00100 | 2022-06-07 | 영업/정상 | 영업
| 2 | restaurants | 3 | 1 | 일반음식점 | 07_24_04_P | 3640000 | 3640000-101-2022-00101 | 2022-06-07 | 영업/정상 | 영업
+----+-------------+-------+----------+------------------------+---------------------------+-------------------------------+------------------------+----------------------+----------------------+-----------------------------
+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------+
| _source.biz_addr | _source.biz_addr_doro | _source.biz_name | _source.biz_domain |
+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------|
| 대전광역시 동구 가양동 292-1 | 대전광역시 동구 동대전로263번길 51 (가양동) | 물회&회덮밥8000원 | 한식 |
| 대전광역시 동구 판암동 839 삼정그린코아 포레스트(1단지) | 대전광역시 동구 동부로10번길 55, 401동 105호 (판암동, 삼정그린코아 포레스트(1단지)) | 비비큐(BBQ) 대전 판암점 | 호프/통닭 |
| 대전광역시 동구 용운동 295-10 | 대전광역시 동구 용운로 159-1, 1층 (용운동) | 라홍방마라탕 대전용운점 | 중국식 |
+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------+
* 원하는 것을 찾아보기
위 소스에 아래 코드를 추가해서 실행해 봅니다.
doc2 = {"query":{"match":{'biz_addr':'대전광역시 동구 가양동'}}}
res2 = es.search(index=es_index, body=doc2, size=10)
df2 = json_normalize(res2['hits']['hits'])
print(tabulate(df2, headers='keys', tablefmt='psql'))
아래 결과를 보면 가장 유사한 것에 _score가 가장 높다.
+----+-------------+-------+----------+------------------------+---------------------------------------------------------|-------------------------------------------------------------------------------------+-------------------------+----------------------+
| | _index | _id | _score | _source.biz_category | _source.biz_addr | _source.biz_addr_doro | _source.biz_name | _source.biz_domain |
|----+-------------+-------+----------+------------------------+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------|
| 0 | restaurants | 1 | 1.31099 | 일반음식점 | 대전광역시 동구 가양동 292-1 | 대전광역시 동구 동대전로263번길 51 (가양동) | 물회&회덮밥8000원 | 한식 |
| 1 | restaurants | 3 | 0.280566 | 일반음식점 | 대전광역시 동구 용운동 295-10 | 대전광역시 동구 용운로 159-1, 1층 (용운동) | 라홍방마라탕 대전용운점 | 중국식 |
| 2 | restaurants | 2 | 0.243613 | 일반음식점 | 대전광역시 동구 판암동 839 삼정그린코아 포레스트(1단지) | 대전광역시 동구 동부로10번길 55, 401동 105호 (판암동, 삼정그린코아 포레스트(1단지)) | 비비큐(BBQ) 대전 판암점 | 호프/통닭 |
+----+-------------+-------+----------+------------------------+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------+
이렇게 주소와 건물명등을 순서없이 입력해도 잘됨
doc3 = {"query":{"match":{'biz_addr':'삼정그린코아 대전광역시 동구 판암동 839 포레스트'}}}
res3 = es.search(index=es_index, body=doc3, size=10)
df3 = json_normalize(res3['hits']['hits'])
print(tabulate(df3, headers='keys', tablefmt='psql'))
+----+------------+-------+----------+------------------------+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------+
| | _index | _id | _score | _source.biz_category | _source.biz_addr | _source.biz_addr_doro | _source.biz_name | _source.biz_domain |
|----+------------+-------+----------+------------------------+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------|
| 0 | rasturants | 2 | 3.82244 | 일반음식점 | 대전광역시 동구 판암동 839 삼정그린코아 포레스트(1단지) | 대전광역시 동구 동부로10번길 55, 401동 105호 (판암동, 삼정그린코아 포레스트(1단지)) | 비비큐(BBQ) 대전 판암점 | 호프/통닭 |
| 1 | rasturants | 1 | 0.280566 | 일반음식점 | 대전광역시 동구 가양동 292-1 | 대전광역시 동구 동대전로263번길 51 (가양동) | 물회&회덮밥8000원 | 한식 |
| 2 | rasturants | 3 | 0.280566 | 일반음식점 | 대전광역시 동구 용운동 295-10 | 대전광역시 동구 용운로 159-1, 1층 (용운동) | 라홍방마라탕 대전용운점 | 중국식 |
+----+------------+-------+----------+------------------------+---------------------------------------------------------+-------------------------------------------------------------------------------------+-------------------------+----------------------+
* mgmt_no 를 키로두고 bulk로 업로드 하는 소스 : elasticsearch_bulk_upload.py
JSON으로 만들어진 데이터를 key를 만들어서 대량으로 업로드하는 것을 만들어 보겠습니다.
벌크 업로드는 1개 업로드 하는 형식과 조금 다르다.
벌크 업로드는 여러가지 방식이 있겠지만 여기서는 인덱스별로 주소데이터를 넣는 방식을 택하겠습니다.
POST /<인덱스명>/_bulk 형식이며 body에 데이터를 넣어주면 됩니다.
PUT customer/_bulk
{ "create": { } }
{ "firstname": "Monica","lastname":"Rambeau"}
{ "create": { } }
{ "firstname": "Carol","lastname":"Danvers"}
{ "create": { } }
{ "firstname": "Wanda","lastname":"Maximoff"}
{ "create": { } }
{ "firstname": "Jennifer","lastname":"Takeda"}
* "index" : {} 이렇게 하면 자동으로 _id 가 부여됩니다. 명시적으로 _id를 설정하려면
{ "index" : { "_id" : "1" } } 해주면 됩니다. index:{} 이렇게만 하면 알아서 id를 부여합니다.
그리고 같은 데이터를 넣어도 _id가 달라집니다. 따라서 업데이트가 필요한 곳은 _id를 명시적으로
부여하는 것이 좋습니다.
"index": {
"_index": "restaurants",
"_id": "j8cqtYYBDm79w2rrCLve",
* 여러줄로 하면 입력오류가 발생합니. 그래서 JSON은 한줄로 해야 합니다.
또한 _id:3 으로 2개를 입력하면 version이 올라 갑니다.
PUT restaurants/_bulk
{ "index" : {"_id":3} }
{ "biz_category":"일반음식점", "biz_category_id":"07_24_04_P", "biz_category_areacd":3640000,
"mgmt_no":"3640000-101-2022-00101","allow_date":"2022-06-07", "biz_status":"영업\/정상",
"biz_status_detail":"영업","biz_addr":"대전광역시 동구 용운동 295-10 ",
"biz_addr_doro":"대전광역시 동구 용운로 159-1, 1층 (용운동)",
"biz_name":"라홍방마라탕 대전용운점","biz_domain":"중국식"}
{ "index" : {"_id":3} }
{ "biz_category":"일반음식점", "biz_category_id":"07_24_04_P", "biz_category_areacd":3640000,
"mgmt_no":"3640000-101-2022-00101","allow_date":"2022-06-17", "biz_status":"영업\/정상",
"biz_status_detail":"영업","biz_addr":"대전광역시 동구 용운동 295-10 ",
"biz_addr_doro":"대전광역시 동구 용운로 159-1, 1층 (용운동)",
"biz_name":"라홍방마라탕 대전용운점","biz_domain":"중국식"}
속성에는 index, delete, create, update 가 있습니다. 이 속성에 따라서 아래 실제 데이터는 약간씩
차이가 있습니다.
그래서 GET /restaurants/_doc/3 로 조회를 해보면 아래와 같이 최근 것만 나옵니다.
{
"_index": "restaurants",
"_id": "3",
"_version": 5,
"_seq_no": 6,
"_primary_term": 1,
"found": true,
"_source": {
"biz_category": "일반음식점",
"biz_category_id": "07_24_04_P",
"biz_category_areacd": 3640000,
"mgmt_no": "3640000-101-2022-00101",
"allow_date": "2022-06-17",
"biz_status": "영업/정상",
"biz_status_detail": "영업",
"biz_addr": "대전광역시 동구 용운동 295-10 ",
"biz_addr_doro": "대전광역시 동구 용운로 159-1, 1층 (용운동)",
"biz_name": "라홍방마라탕 대전용운점",
"biz_domain": "중국식"
}
}
만약 동일 주소에 다른 음식점으로 변경된 것을 알기위해 이력관리를 해야 한다면 _id를 동일하게
입력해주면 좋습니다.
아래는 도로명주소, 지번주소를 검색하는 함수입니다.
def search_doro_addr(esindex:str, keyword:str):
doc = {"query":{"match":{'biz_addr_doro':keyword}}}
res = es.search(index=esindex, body=doc, size=10)
df = json_normalize(res['hits']['hits'])
print(tabulate(df, headers='keys', tablefmt='psql'))
def search_jibun_addr(esindex:str, keyword:str):
doc = {"query":{"match":{'biz_addr':keyword}}}
res = es.search(index=esindex, body=doc, size=10)
df = json_normalize(res['hits']['hits'])
print(tabulate(df, headers='keys', tablefmt='psql'))
search_doro_addr('local_restaurants', '대전광역시 중구 당디로 61')
search_jibun_addr('local_restaurants', '대전광역시 동구 판암동 839 ')
다음 3탄에서는 excel을 일어서 bulk.json을 생성하고 elastic search에 업로드 후 검색을 해보도록
하겠습니다.
다음편에 계속 <갈길이 멀다.. 힘내자>
댓글
댓글 쓰기