UI 개발 (feat. Streamlit)
개발을 진행하면서 CMD로 요청을 보내고 응답을 받는 과정이 번거롭고, 가독성도 좋지 않아서 간단한 UI를 개발했다.
스택은 streamlit을 활용했다.
(react를 써보고 싶었으나, 시간적 이슈로 인해 간단한 방법을 선택했다.. react는 추후 포스팅할 프로젝트에서 다룰 예정이다.. ㅎ)
streamlit 또한 컨테이너로 만들어서 다른 서비스와 함께 도커 컴포즈로 개발할 것이다.
프로젝트 구조는 다음과 같다.

Dockerfile (Streamlit)
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
requirements.txt (Streamlit)
streamlit
requests
streamlit_app.py
import streamlit as st
import requests
# FastAPI 백엔드 URL (컨테이너 네트워크에서 FastAPI와 통신하기 위함)
BACKEND_URL = "http://fastapi:8000/search"
st.title("📚 BioResearch-Paper Search Engine")
st.write("Elasticsearch 기반 bio 논문 검색")
# 검색 입력창
query = st.text_input("검색어를 입력하세요", "")
# 저자 검색 옵션
author = st.text_input("저자 이름을 입력하세요", "")
# 검색 버튼
if st.button("검색"):
params = {"query": query}
if author:
params["author"] = author
response = requests.get(BACKEND_URL, params=params)
if response.status_code == 200:
results = response.json().get("results", [])
if results:
st.write(f"🔍 {len(results)}개의 논문이 검색되었습니다.")
for paper in results:
source = paper["_source"]
st.subheader(source["title"])
st.write(f"**저자:** {', '.join([author['name'] for author in source['authors']])}")
st.write(f"📅 **출판일:** {source['publication_date']}")
st.write(f"📖 **요약:** {source['abstract'][:500]}...")
st.write(f"[🔗 논문 링크]({source['url']})")
st.write("---")
else:
st.warning("검색 결과가 없습니다")
else:
st.error("검색 요청에 실패했습니다")
수정된 docker-compose.yml (Streamlit 서비스 추가)
version: '3.7'
services:
# PostgreSQL (Airflow metadata DB)
postgres:
image: postgres:13
container_name: postgres_db
restart: always
environment:
POSTGRES_USER: airflow
POSTGRES_PASSWORD: airflow
POSTGRES_DB: airflow
ports:
- "5432:5432"
networks:
- elastic_network
volumes:
- postgres_data:/var/lib/postgresql/data
# Elasticsearch
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
- "9300:9300"
networks:
- elastic_network
# Kibana
kibana:
image: docker.elastic.co/kibana/kibana:8.11.0
container_name: kibana
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- elastic_network
# FastAPI
fastapi:
build: ../fastapi
container_name: fastapi
ports:
- "8000:8000"
depends_on:
- elasticsearch
networks:
- elastic_network
# Airflow
airflow:
build: ../airflow
container_name: airflow
restart: always
ports:
- "8080:8080"
depends_on:
- elasticsearch
- postgres
environment:
- AIRFLOW_HOME=/opt/airflow
- AIRFLOW__CORE__EXECUTOR=LocalExecutor
- AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=postgresql+psycopg2://airflow:airflow@postgres:5432/airflow
- AIRFLOW__CORE__LOAD_EXAMPLES=False
volumes:
- airflow_db:/opt/airflow
- ../airflow/dags:/opt/airflow/dags # DAG 폴더 마운트
networks:
- elastic_network
# Streamlit
streamlit:
build: ../streamlit
container_name: streamlit
ports:
- "8501:8501"
depends_on:
- fastapi
- elasticsearch
networks:
- elastic_network
networks:
elastic_network:
driver: bridge
volumes:
postgres_data:
airflow_db:
위 코드를 모두 작성 후 컨테이너를 종료한 뒤 다시 시작해준다.
docker-compose down
docker-compose up --build -d
Streamlit 접속 및 검색 테스트
http://localhost:8501에 접속하자.

- 검색 테스트 1: 기본 검색에 "COVID-19" 입력 후 검색 수행


논문 링크를 클릭하면 해당 논문 페이지로 이동한다!
- 검색 테스트 2: 저자 검색

저자 풀네임을 입력해도 검색 결과가 없다는 메시지가 나와서 CMD로 요청을 보내봤다.

curl 요청 결과를 분석한 결과, "query" 필드가 필수로 설정되어 있어서 author 검색만 수행할 때 에러가 발생한다.
FastAPI의 Query 매개변수에서 query: str을 필수 값으로 받고 있어서 발생한 오류로 추정했다.
FastAPI app.py 수정
query를 선택적 (Optional) 매개변수로 변경해주자.
from fastapi import FastAPI, Query
from elasticsearch import Elasticsearch
app = FastAPI()
es = Elasticsearch("http://elasticsearch:9200")
@app.get("/search")
async def search_papers(
query: str = Query(None, description="Search by keyword"),
author: str = Query(None, description="Search by author's name"),
sort_by: str = Query("publication_date", description="Sort field"),
order: str = Query("desc", description="Sort order ('asc' or 'desc')")
):
"""Elasticsearch에서 논문 검색 수행"""
must_clauses = []
# 키워드 검색 추가 (query가 존재하는 경우)
if query:
must_clauses.append(
{"multi_match": {
"query": query,
"fields": ["title", "abstract", "category"]
}}
)
# 저자 검색 추가 (author가 존재하는 경우)
if author:
must_clauses.append(
{
"nested": {
"path": "authors",
"query": {
"match": {
"authors.name": author
}
}
}
}
)
# 검색어 또는 저자 중 하나는 반드시 입력해야 함
if not must_clauses:
return {"error": "검색어 또는 저자를 입력해야 합니다."}
query_body = {
"query": {
"bool": {
"must": must_clauses
}
},
"sort": [
{sort_by: {"order": order}}
]
}
response = es.search(index="research_papers", body=query_body)
return {"results": response["hits"]["hits"]}
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI!"}
다시 저자 검색 테스트를 수행했다.

저자 검색도 성공적으로 작동한다!
'Data Engineering > Elasticsearch' 카테고리의 다른 글
| [Opensearch] Opensearch 개념 정리 (0) | 2025.02.11 |
|---|---|
| [Elasticsearch] 논문 검색 엔진 구현 프로젝트 (Elasticsearch + Airflow + FastAPI) (4) (0) | 2025.02.06 |
| [Elasticsearch] 논문 검색 엔진 구현 프로젝트 (Elasticsearch + Airflow + FastAPI) (2) (0) | 2025.02.05 |
| [Elasticsearch] 논문 검색 엔진 구현 프로젝트 (Elasticsearch + Airflow + FastAPI) (1) (0) | 2025.02.04 |
| [Elasticsearch] Elasticsearch 기본 개념 (0) | 2025.02.04 |