기본 콘텐츠로 건너뛰기

주소로 고객 검색 서비스 구축하기(feat. Elastic Search v 8.6.2, MacOS) - 2탄

주소로 고객 검색 서비스 구축하기(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으로 만들어서 
     넣겠습니다.


2) 주소 데이터 엑셀을 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에 업로드 후 검색을 해보도록 하겠습니다.



다음편에 계속 <갈길이 멀다.. 힘내자>

댓글

이 블로그의 인기 게시물

[2024-10-19] iPhone, iPad에서 ChatGPT로 PDF 생성시 한글 깨짐 해결 방법

iPhone, iPad에서 ChatGPT로 PDF 생성 시 한글 깨짐 해결 방법

[quaser.dev][2014-09-14] 윈도우즈(10, 64bit)에 개발환경 설정하기

[quaser.dev][2014-09-14] 윈도우즈(10, 64bit)에 개발환경 설정하기

[2025-04-16(수)] OpenAI gpt-4.1 시리즈 발표, Anthropic Claude에 대한 생각

OpenAI gpt-4.1 시리즈 발표, Anthropic Claude에 대한 생각 안녕하세요. 클스 입니다. 4/15일자로 openai가 gpt-4.1 시리즈를 발표 했습니다. 현재는 api로만 사용가능합니다. 점차 웹/앱 사용자에게 오픈 될거라 생각 됩니다. 비용상 문제로 4.1-mini, nano를 사용해서 chatbot을 만들어 보고 있습니다. 4o 시리즈 보다는 확실히 빠르고, 답변의 정확도는 올라간 것 같습니다. 앤트로픽 클로드와 비교를 많이 하는데, 업무 시스템 혹은 AI 솔루션을 개발하는 입장에서는 어떤 생태계를 제공하는가가 주요한 결정 입니다. AI관련 인력을 충분히 보유한 회사의 경우는 어떤걸 사용해도 좋을 결과를 가지겠지만 일반적인 챗봇 개발 절차를 보면 다음과 같이 볼 수 있습니다. 1. 문서를 준비한다. 대부분 pdf, text, markdown 2. 문서를 파싱해서 vectordb에 올린다.     - 별도 벡터디비 구성 필요. 어떤 db를 선택할지 고민 필요     - 어떤 Parser를 사용할지, 텍스트 오버래핑은 얼마가 적당한지 고민 필요        (회사의 문서가 워낙 많고, 다양하면 하나하나 테스트 해서 좋은걸 선택하는 것이 어렵다)     - 유사도 측정은 어떤 알고리즘을 써야할지 고민 필요     - llamaindex도 고민해야 함. 3. RAG flow를 만든다.     - langchain을 쓸지, 각 AI 벤더에서 제공하는 sdk를 쓸지 고민 필요       (대부분 락인이 되지 않으려면 langchain을 사용하면 좋지만, 벤더에 특화면 기능 적용이 늦음) 4. 챗봇 UI 앱을 만든다.     - 답변이 text 로 구성되다 보니. 그래프, 이미지등 복합적인 컨텐츠를 재배치 하여 표현하기 상당히 어렵네요. (이건 제가 실력이 모자라서 .. 패스) ...